github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/refactoring/move_validate_test.go (about)

     1  package refactoring
     2  
     3  import (
     4  	"strings"
     5  	"testing"
     6  
     7  	"github.com/hashicorp/hcl/v2"
     8  	"github.com/hashicorp/hcl/v2/hclsyntax"
     9  	"github.com/hashicorp/terraform/internal/addrs"
    10  	"github.com/hashicorp/terraform/internal/configs"
    11  	"github.com/hashicorp/terraform/internal/configs/configload"
    12  	"github.com/hashicorp/terraform/internal/initwd"
    13  	"github.com/hashicorp/terraform/internal/instances"
    14  	"github.com/hashicorp/terraform/internal/registry"
    15  	"github.com/hashicorp/terraform/internal/tfdiags"
    16  	"github.com/zclconf/go-cty/cty/gocty"
    17  )
    18  
    19  func TestValidateMoves(t *testing.T) {
    20  	rootCfg, instances := loadRefactoringFixture(t, "testdata/move-validate-zoo")
    21  
    22  	tests := map[string]struct {
    23  		Statements []MoveStatement
    24  		WantError  string
    25  	}{
    26  		"no move statements": {
    27  			Statements: nil,
    28  			WantError:  ``,
    29  		},
    30  		"some valid statements": {
    31  			Statements: []MoveStatement{
    32  				// This is just a grab bag of various valid cases that don't
    33  				// generate any errors at all.
    34  				makeTestMoveStmt(t,
    35  					``,
    36  					`test.nonexist1`,
    37  					`test.target1`,
    38  				),
    39  				makeTestMoveStmt(t,
    40  					`single`,
    41  					`test.nonexist1`,
    42  					`test.target1`,
    43  				),
    44  				makeTestMoveStmt(t,
    45  					``,
    46  					`test.nonexist2`,
    47  					`module.nonexist.test.nonexist2`,
    48  				),
    49  				makeTestMoveStmt(t,
    50  					``,
    51  					`module.single.test.nonexist3`,
    52  					`module.single.test.single`,
    53  				),
    54  				makeTestMoveStmt(t,
    55  					``,
    56  					`module.single.test.nonexist4`,
    57  					`test.target2`,
    58  				),
    59  				makeTestMoveStmt(t,
    60  					``,
    61  					`test.single[0]`, // valid because test.single doesn't have "count" set
    62  					`test.target3`,
    63  				),
    64  				makeTestMoveStmt(t,
    65  					``,
    66  					`test.zero_count[0]`, // valid because test.zero_count has count = 0
    67  					`test.target4`,
    68  				),
    69  				makeTestMoveStmt(t,
    70  					``,
    71  					`test.zero_count[1]`, // valid because test.zero_count has count = 0
    72  					`test.zero_count[0]`,
    73  				),
    74  				makeTestMoveStmt(t,
    75  					``,
    76  					`module.nonexist1`,
    77  					`module.target3`,
    78  				),
    79  				makeTestMoveStmt(t,
    80  					``,
    81  					`module.nonexist1[0]`,
    82  					`module.target4`,
    83  				),
    84  				makeTestMoveStmt(t,
    85  					``,
    86  					`module.single[0]`, // valid because module.single doesn't have "count" set
    87  					`module.target5`,
    88  				),
    89  				makeTestMoveStmt(t,
    90  					``,
    91  					`module.for_each["nonexist1"]`,
    92  					`module.for_each["a"]`,
    93  				),
    94  				makeTestMoveStmt(t,
    95  					``,
    96  					`module.for_each["nonexist2"]`,
    97  					`module.nonexist.module.nonexist`,
    98  				),
    99  				makeTestMoveStmt(t,
   100  					``,
   101  					`module.for_each["nonexist3"].test.single`, // valid because module.for_each doesn't currently have a "nonexist3"
   102  					`module.for_each["a"].test.single`,
   103  				),
   104  			},
   105  			WantError: ``,
   106  		},
   107  		"two statements with the same endpoints": {
   108  			Statements: []MoveStatement{
   109  				makeTestMoveStmt(t,
   110  					``,
   111  					`module.a`,
   112  					`module.b`,
   113  				),
   114  				makeTestMoveStmt(t,
   115  					``,
   116  					`module.a`,
   117  					`module.b`,
   118  				),
   119  			},
   120  			WantError: ``,
   121  		},
   122  		"moving nowhere": {
   123  			Statements: []MoveStatement{
   124  				makeTestMoveStmt(t,
   125  					``,
   126  					`module.a`,
   127  					`module.a`,
   128  				),
   129  			},
   130  			WantError: `Redundant move statement: This statement declares a move from module.a to the same address, which is the same as not declaring this move at all.`,
   131  		},
   132  		/*
   133  			// TODO: This test can't pass until we've implemented
   134  			// addrs.MoveEndpointInModule.CanChainFrom, which is what
   135  			// detects the chaining condition this is testing for.
   136  			"cyclic chain": {
   137  				Statements: []MoveStatement{
   138  					makeTestMoveStmt(t,
   139  						``,
   140  						`module.a`,
   141  						`module.b`,
   142  					),
   143  					makeTestMoveStmt(t,
   144  						``,
   145  						`module.b`,
   146  						`module.c`,
   147  					),
   148  					makeTestMoveStmt(t,
   149  						``,
   150  						`module.c`,
   151  						`module.a`,
   152  					),
   153  				},
   154  				WantError: `bad cycle`,
   155  			},
   156  		*/
   157  		"module.single as a call still exists in configuration": {
   158  			Statements: []MoveStatement{
   159  				makeTestMoveStmt(t,
   160  					``,
   161  					`module.single`,
   162  					`module.other`,
   163  				),
   164  			},
   165  			WantError: `Moved object still exists: This statement declares a move from module.single, but that module call is still declared at testdata/move-validate-zoo/move-validate-root.tf:6,1.
   166  
   167  Change your configuration so that this call will be declared as module.other instead.`,
   168  		},
   169  		"module.single as an instance still exists in configuration": {
   170  			Statements: []MoveStatement{
   171  				makeTestMoveStmt(t,
   172  					``,
   173  					`module.single`,
   174  					`module.other[0]`,
   175  				),
   176  			},
   177  			WantError: `Moved object still exists: This statement declares a move from module.single, but that module instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:6,1.
   178  
   179  Change your configuration so that this instance will be declared as module.other[0] instead.`,
   180  		},
   181  		"module.count[0] still exists in configuration": {
   182  			Statements: []MoveStatement{
   183  				makeTestMoveStmt(t,
   184  					``,
   185  					`module.count[0]`,
   186  					`module.other`,
   187  				),
   188  			},
   189  			WantError: `Moved object still exists: This statement declares a move from module.count[0], but that module instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:12,12.
   190  
   191  Change your configuration so that this instance will be declared as module.other instead.`,
   192  		},
   193  		`module.for_each["a"] still exists in configuration`: {
   194  			Statements: []MoveStatement{
   195  				makeTestMoveStmt(t,
   196  					``,
   197  					`module.for_each["a"]`,
   198  					`module.other`,
   199  				),
   200  			},
   201  			WantError: `Moved object still exists: This statement declares a move from module.for_each["a"], but that module instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:22,14.
   202  
   203  Change your configuration so that this instance will be declared as module.other instead.`,
   204  		},
   205  		"test.single as a resource still exists in configuration": {
   206  			Statements: []MoveStatement{
   207  				makeTestMoveStmt(t,
   208  					``,
   209  					`test.single`,
   210  					`test.other`,
   211  				),
   212  			},
   213  			WantError: `Moved object still exists: This statement declares a move from test.single, but that resource is still declared at testdata/move-validate-zoo/move-validate-root.tf:27,1.
   214  
   215  Change your configuration so that this resource will be declared as test.other instead.`,
   216  		},
   217  		"test.single as an instance still exists in configuration": {
   218  			Statements: []MoveStatement{
   219  				makeTestMoveStmt(t,
   220  					``,
   221  					`test.single`,
   222  					`test.other[0]`,
   223  				),
   224  			},
   225  			WantError: `Moved object still exists: This statement declares a move from test.single, but that resource instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:27,1.
   226  
   227  Change your configuration so that this instance will be declared as test.other[0] instead.`,
   228  		},
   229  		"module.single.test.single as a resource still exists in configuration": {
   230  			Statements: []MoveStatement{
   231  				makeTestMoveStmt(t,
   232  					``,
   233  					`module.single.test.single`,
   234  					`test.other`,
   235  				),
   236  			},
   237  			WantError: `Moved object still exists: This statement declares a move from module.single.test.single, but that resource is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1.
   238  
   239  Change your configuration so that this resource will be declared as test.other instead.`,
   240  		},
   241  		"module.single.test.single as a resource declared in module.single still exists in configuration": {
   242  			Statements: []MoveStatement{
   243  				makeTestMoveStmt(t,
   244  					`single`,
   245  					`test.single`,
   246  					`test.other`,
   247  				),
   248  			},
   249  			WantError: `Moved object still exists: This statement declares a move from module.single.test.single, but that resource is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1.
   250  
   251  Change your configuration so that this resource will be declared as module.single.test.other instead.`,
   252  		},
   253  		"module.single.test.single as an instance still exists in configuration": {
   254  			Statements: []MoveStatement{
   255  				makeTestMoveStmt(t,
   256  					``,
   257  					`module.single.test.single`,
   258  					`test.other[0]`,
   259  				),
   260  			},
   261  			WantError: `Moved object still exists: This statement declares a move from module.single.test.single, but that resource instance is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1.
   262  
   263  Change your configuration so that this instance will be declared as test.other[0] instead.`,
   264  		},
   265  		"module.count[0].test.single still exists in configuration": {
   266  			Statements: []MoveStatement{
   267  				makeTestMoveStmt(t,
   268  					``,
   269  					`module.count[0].test.single`,
   270  					`test.other`,
   271  				),
   272  			},
   273  			WantError: `Moved object still exists: This statement declares a move from module.count[0].test.single, but that resource is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1.
   274  
   275  Change your configuration so that this resource will be declared as test.other instead.`,
   276  		},
   277  		"two different moves from test.nonexist": {
   278  			Statements: []MoveStatement{
   279  				makeTestMoveStmt(t,
   280  					``,
   281  					`test.nonexist`,
   282  					`test.other1`,
   283  				),
   284  				makeTestMoveStmt(t,
   285  					``,
   286  					`test.nonexist`,
   287  					`test.other2`,
   288  				),
   289  			},
   290  			WantError: `Ambiguous move statements: A statement at test:1,1 declared that test.nonexist moved to test.other1, but this statement instead declares that it moved to test.other2.
   291  
   292  Each resource can move to only one destination resource.`,
   293  		},
   294  		"two different moves to test.single": {
   295  			Statements: []MoveStatement{
   296  				makeTestMoveStmt(t,
   297  					``,
   298  					`test.other1`,
   299  					`test.single`,
   300  				),
   301  				makeTestMoveStmt(t,
   302  					``,
   303  					`test.other2`,
   304  					`test.single`,
   305  				),
   306  			},
   307  			WantError: `Ambiguous move statements: A statement at test:1,1 declared that test.other1 moved to test.single, but this statement instead declares that test.other2 moved there.
   308  
   309  Each resource can have moved from only one source resource.`,
   310  		},
   311  		"two different moves to module.count[0].test.single across two modules": {
   312  			Statements: []MoveStatement{
   313  				makeTestMoveStmt(t,
   314  					``,
   315  					`test.other1`,
   316  					`module.count[0].test.single`,
   317  				),
   318  				makeTestMoveStmt(t,
   319  					`count`,
   320  					`test.other2`,
   321  					`test.single`,
   322  				),
   323  			},
   324  			WantError: `Ambiguous move statements: A statement at test:1,1 declared that test.other1 moved to module.count[0].test.single, but this statement instead declares that module.count[0].test.other2 moved there.
   325  
   326  Each resource can have moved from only one source resource.`,
   327  		},
   328  		/*
   329  					// FIXME: This rule requires a deeper analysis to understand that
   330  					// module.single already contains a test.single and thus moving
   331  					// it to module.foo implicitly also moves module.single.test.single
   332  					// module.foo.test.single.
   333  					"two different moves to nested test.single by different paths": {
   334  						Statements: []MoveStatement{
   335  							makeTestMoveStmt(t,
   336  								``,
   337  								`test.beep`,
   338  								`module.foo.test.single`,
   339  							),
   340  							makeTestMoveStmt(t,
   341  								``,
   342  								`module.single`,
   343  								`module.foo`,
   344  							),
   345  						},
   346  						WantError: `Ambiguous move statements: A statement at test:1,1 declared that test.beep moved to module.foo.test.single, but this statement instead declares that module.single.test.single moved there.
   347  
   348  			Each resource can have moved from only one source resource.`,
   349  					},
   350  		*/
   351  		"move from resource in another module package": {
   352  			Statements: []MoveStatement{
   353  				makeTestMoveStmt(t,
   354  					``,
   355  					`module.fake_external.test.thing`,
   356  					`test.thing`,
   357  				),
   358  			},
   359  			WantError: `Cross-package move statement: This statement declares a move from an object declared in external module package "fake-external:///". Move statements can be only within a single module package.`,
   360  		},
   361  		"move to resource in another module package": {
   362  			Statements: []MoveStatement{
   363  				makeTestMoveStmt(t,
   364  					``,
   365  					`test.thing`,
   366  					`module.fake_external.test.thing`,
   367  				),
   368  			},
   369  			WantError: `Cross-package move statement: This statement declares a move to an object declared in external module package "fake-external:///". Move statements can be only within a single module package.`,
   370  		},
   371  		"move from module call in another module package": {
   372  			Statements: []MoveStatement{
   373  				makeTestMoveStmt(t,
   374  					``,
   375  					`module.fake_external.module.a`,
   376  					`module.b`,
   377  				),
   378  			},
   379  			WantError: `Cross-package move statement: This statement declares a move from an object declared in external module package "fake-external:///". Move statements can be only within a single module package.`,
   380  		},
   381  		"move to module call in another module package": {
   382  			Statements: []MoveStatement{
   383  				makeTestMoveStmt(t,
   384  					``,
   385  					`module.a`,
   386  					`module.fake_external.module.b`,
   387  				),
   388  			},
   389  			WantError: `Cross-package move statement: This statement declares a move to an object declared in external module package "fake-external:///". Move statements can be only within a single module package.`,
   390  		},
   391  		"move to a call that refers to another module package": {
   392  			Statements: []MoveStatement{
   393  				makeTestMoveStmt(t,
   394  					``,
   395  					`module.nonexist`,
   396  					`module.fake_external`,
   397  				),
   398  			},
   399  			WantError: ``, // This is okay because the call itself is not considered to be inside the package it refers to
   400  		},
   401  		"move to instance of a call that refers to another module package": {
   402  			Statements: []MoveStatement{
   403  				makeTestMoveStmt(t,
   404  					``,
   405  					`module.nonexist`,
   406  					`module.fake_external[0]`,
   407  				),
   408  			},
   409  			WantError: ``, // This is okay because the call itself is not considered to be inside the package it refers to
   410  		},
   411  	}
   412  
   413  	for name, test := range tests {
   414  		t.Run(name, func(t *testing.T) {
   415  			gotDiags := ValidateMoves(test.Statements, rootCfg, instances)
   416  
   417  			switch {
   418  			case test.WantError != "":
   419  				if !gotDiags.HasErrors() {
   420  					t.Fatalf("unexpected success\nwant error: %s", test.WantError)
   421  				}
   422  				if got, want := gotDiags.Err().Error(), test.WantError; got != want {
   423  					t.Fatalf("wrong error\ngot error:  %s\nwant error: %s", got, want)
   424  				}
   425  			default:
   426  				if gotDiags.HasErrors() {
   427  					t.Fatalf("unexpected error\ngot error: %s", gotDiags.Err().Error())
   428  				}
   429  			}
   430  		})
   431  	}
   432  }
   433  
   434  // loadRefactoringFixture reads a configuration from the given directory and
   435  // does some naive static processing on any count and for_each expressions
   436  // inside, in order to get a realistic-looking instances.Set for what it
   437  // declares without having to run a full Terraform plan.
   438  func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instances.Set) {
   439  	t.Helper()
   440  
   441  	loader, cleanup := configload.NewLoaderForTests(t)
   442  	defer cleanup()
   443  
   444  	inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil))
   445  	_, instDiags := inst.InstallModules(dir, true, initwd.ModuleInstallHooksImpl{})
   446  	if instDiags.HasErrors() {
   447  		t.Fatal(instDiags.Err())
   448  	}
   449  
   450  	// Since module installer has modified the module manifest on disk, we need
   451  	// to refresh the cache of it in the loader.
   452  	if err := loader.RefreshModules(); err != nil {
   453  		t.Fatalf("failed to refresh modules after installation: %s", err)
   454  	}
   455  
   456  	rootCfg, diags := loader.LoadConfig(dir)
   457  	if diags.HasErrors() {
   458  		t.Fatalf("failed to load root module: %s", diags.Error())
   459  	}
   460  
   461  	expander := instances.NewExpander()
   462  	staticPopulateExpanderModule(t, rootCfg, addrs.RootModuleInstance, expander)
   463  	return rootCfg, expander.AllInstances()
   464  }
   465  
   466  func staticPopulateExpanderModule(t *testing.T, rootCfg *configs.Config, moduleAddr addrs.ModuleInstance, expander *instances.Expander) {
   467  	t.Helper()
   468  
   469  	modCfg := rootCfg.DescendentForInstance(moduleAddr)
   470  	if modCfg == nil {
   471  		t.Fatalf("no configuration for %s", moduleAddr)
   472  	}
   473  
   474  	if len(modCfg.Path) > 0 && modCfg.Path[len(modCfg.Path)-1] == "fake_external" {
   475  		// As a funny special case we modify the source address of this
   476  		// module to be something that counts as a separate package,
   477  		// so we can test rules relating to crossing package boundaries
   478  		// even though we really just loaded the module from a local path.
   479  		modCfg.SourceAddr = fakeExternalModuleSource
   480  	}
   481  
   482  	for _, call := range modCfg.Module.ModuleCalls {
   483  		callAddr := addrs.ModuleCall{Name: call.Name}
   484  
   485  		if call.Name == "fake_external" {
   486  			// As a funny special case we modify the source address of this
   487  			// module to be something that counts as a separate package,
   488  			// so we can test rules relating to crossing package boundaries
   489  			// even though we really just loaded the module from a local path.
   490  			call.SourceAddr = fakeExternalModuleSource
   491  		}
   492  
   493  		// In order to get a valid, useful set of instances here we're going
   494  		// to just statically evaluate the count and for_each expressions.
   495  		// Normally it's valid to use references and functions there, but for
   496  		// our unit tests we'll just limit it to literal values to avoid
   497  		// bringing all of the core evaluator complexity.
   498  		switch {
   499  		case call.ForEach != nil:
   500  			val, diags := call.ForEach.Value(nil)
   501  			if diags.HasErrors() {
   502  				t.Fatalf("invalid for_each: %s", diags.Error())
   503  			}
   504  			expander.SetModuleForEach(moduleAddr, callAddr, val.AsValueMap())
   505  		case call.Count != nil:
   506  			val, diags := call.Count.Value(nil)
   507  			if diags.HasErrors() {
   508  				t.Fatalf("invalid count: %s", diags.Error())
   509  			}
   510  			var count int
   511  			err := gocty.FromCtyValue(val, &count)
   512  			if err != nil {
   513  				t.Fatalf("invalid count at %s: %s", call.Count.Range(), err)
   514  			}
   515  			expander.SetModuleCount(moduleAddr, callAddr, count)
   516  		default:
   517  			expander.SetModuleSingle(moduleAddr, callAddr)
   518  		}
   519  
   520  		// We need to recursively analyze the child modules too.
   521  		calledMod := modCfg.Path.Child(call.Name)
   522  		for _, inst := range expander.ExpandModule(calledMod) {
   523  			staticPopulateExpanderModule(t, rootCfg, inst, expander)
   524  		}
   525  	}
   526  
   527  	for _, rc := range modCfg.Module.ManagedResources {
   528  		staticPopulateExpanderResource(t, moduleAddr, rc, expander)
   529  	}
   530  	for _, rc := range modCfg.Module.DataResources {
   531  		staticPopulateExpanderResource(t, moduleAddr, rc, expander)
   532  	}
   533  
   534  }
   535  
   536  func staticPopulateExpanderResource(t *testing.T, moduleAddr addrs.ModuleInstance, rCfg *configs.Resource, expander *instances.Expander) {
   537  	t.Helper()
   538  
   539  	addr := rCfg.Addr()
   540  	switch {
   541  	case rCfg.ForEach != nil:
   542  		val, diags := rCfg.ForEach.Value(nil)
   543  		if diags.HasErrors() {
   544  			t.Fatalf("invalid for_each: %s", diags.Error())
   545  		}
   546  		expander.SetResourceForEach(moduleAddr, addr, val.AsValueMap())
   547  	case rCfg.Count != nil:
   548  		val, diags := rCfg.Count.Value(nil)
   549  		if diags.HasErrors() {
   550  			t.Fatalf("invalid count: %s", diags.Error())
   551  		}
   552  		var count int
   553  		err := gocty.FromCtyValue(val, &count)
   554  		if err != nil {
   555  			t.Fatalf("invalid count at %s: %s", rCfg.Count.Range(), err)
   556  		}
   557  		expander.SetResourceCount(moduleAddr, addr, count)
   558  	default:
   559  		expander.SetResourceSingle(moduleAddr, addr)
   560  	}
   561  }
   562  
   563  func makeTestMoveStmt(t *testing.T, moduleStr, fromStr, toStr string) MoveStatement {
   564  	t.Helper()
   565  
   566  	module := addrs.RootModule
   567  	if moduleStr != "" {
   568  		module = addrs.Module(strings.Split(moduleStr, "."))
   569  	}
   570  
   571  	traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(fromStr), "", hcl.InitialPos)
   572  	if hclDiags.HasErrors() {
   573  		t.Fatalf("invalid from address: %s", hclDiags.Error())
   574  	}
   575  	fromEP, diags := addrs.ParseMoveEndpoint(traversal)
   576  	if diags.HasErrors() {
   577  		t.Fatalf("invalid from address: %s", diags.Err().Error())
   578  	}
   579  
   580  	traversal, hclDiags = hclsyntax.ParseTraversalAbs([]byte(toStr), "", hcl.InitialPos)
   581  	if hclDiags.HasErrors() {
   582  		t.Fatalf("invalid to address: %s", hclDiags.Error())
   583  	}
   584  	toEP, diags := addrs.ParseMoveEndpoint(traversal)
   585  	if diags.HasErrors() {
   586  		t.Fatalf("invalid to address: %s", diags.Err().Error())
   587  	}
   588  
   589  	fromInModule, toInModule := addrs.UnifyMoveEndpoints(module, fromEP, toEP)
   590  	if fromInModule == nil || toInModule == nil {
   591  		t.Fatalf("incompatible move endpoints")
   592  	}
   593  
   594  	return MoveStatement{
   595  		From: fromInModule,
   596  		To:   toInModule,
   597  		DeclRange: tfdiags.SourceRange{
   598  			Filename: "test",
   599  			Start:    tfdiags.SourcePos{Line: 1, Column: 1},
   600  			End:      tfdiags.SourcePos{Line: 1, Column: 1},
   601  		},
   602  	}
   603  }
   604  
   605  var fakeExternalModuleSource = addrs.ModuleSourceRemote{
   606  	PackageAddr: addrs.ModulePackage("fake-external:///"),
   607  }