github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/lang/eval_test.go (about)

     1  package lang
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"testing"
     7  
     8  	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
     9  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema"
    10  
    11  	"github.com/hashicorp/hcl/v2"
    12  	"github.com/hashicorp/hcl/v2/hclsyntax"
    13  
    14  	"github.com/zclconf/go-cty/cty"
    15  	ctyjson "github.com/zclconf/go-cty/cty/json"
    16  )
    17  
    18  func TestScopeEvalContext(t *testing.T) {
    19  	data := &dataForTests{
    20  		CountAttrs: map[string]cty.Value{
    21  			"index": cty.NumberIntVal(0),
    22  		},
    23  		ForEachAttrs: map[string]cty.Value{
    24  			"key":   cty.StringVal("a"),
    25  			"value": cty.NumberIntVal(1),
    26  		},
    27  		Resources: map[string]cty.Value{
    28  			"null_resource.foo": cty.ObjectVal(map[string]cty.Value{
    29  				"attr": cty.StringVal("bar"),
    30  			}),
    31  			"data.null_data_source.foo": cty.ObjectVal(map[string]cty.Value{
    32  				"attr": cty.StringVal("bar"),
    33  			}),
    34  			"null_resource.multi": cty.TupleVal([]cty.Value{
    35  				cty.ObjectVal(map[string]cty.Value{
    36  					"attr": cty.StringVal("multi0"),
    37  				}),
    38  				cty.ObjectVal(map[string]cty.Value{
    39  					"attr": cty.StringVal("multi1"),
    40  				}),
    41  			}),
    42  			"null_resource.each": cty.ObjectVal(map[string]cty.Value{
    43  				"each0": cty.ObjectVal(map[string]cty.Value{
    44  					"attr": cty.StringVal("each0"),
    45  				}),
    46  				"each1": cty.ObjectVal(map[string]cty.Value{
    47  					"attr": cty.StringVal("each1"),
    48  				}),
    49  			}),
    50  			"null_resource.multi[1]": cty.ObjectVal(map[string]cty.Value{
    51  				"attr": cty.StringVal("multi1"),
    52  			}),
    53  		},
    54  		LocalValues: map[string]cty.Value{
    55  			"foo": cty.StringVal("bar"),
    56  		},
    57  		Modules: map[string]cty.Value{
    58  			"module.foo": cty.ObjectVal(map[string]cty.Value{
    59  				"output0": cty.StringVal("bar0"),
    60  				"output1": cty.StringVal("bar1"),
    61  			}),
    62  		},
    63  		PathAttrs: map[string]cty.Value{
    64  			"module": cty.StringVal("foo/bar"),
    65  		},
    66  		TerraformAttrs: map[string]cty.Value{
    67  			"workspace": cty.StringVal("default"),
    68  		},
    69  		InputVariables: map[string]cty.Value{
    70  			"baz": cty.StringVal("boop"),
    71  		},
    72  	}
    73  
    74  	tests := []struct {
    75  		Expr string
    76  		Want map[string]cty.Value
    77  	}{
    78  		{
    79  			`12`,
    80  			map[string]cty.Value{},
    81  		},
    82  		{
    83  			`count.index`,
    84  			map[string]cty.Value{
    85  				"count": cty.ObjectVal(map[string]cty.Value{
    86  					"index": cty.NumberIntVal(0),
    87  				}),
    88  			},
    89  		},
    90  		{
    91  			`each.key`,
    92  			map[string]cty.Value{
    93  				"each": cty.ObjectVal(map[string]cty.Value{
    94  					"key": cty.StringVal("a"),
    95  				}),
    96  			},
    97  		},
    98  		{
    99  			`each.value`,
   100  			map[string]cty.Value{
   101  				"each": cty.ObjectVal(map[string]cty.Value{
   102  					"value": cty.NumberIntVal(1),
   103  				}),
   104  			},
   105  		},
   106  		{
   107  			`local.foo`,
   108  			map[string]cty.Value{
   109  				"local": cty.ObjectVal(map[string]cty.Value{
   110  					"foo": cty.StringVal("bar"),
   111  				}),
   112  			},
   113  		},
   114  		{
   115  			`null_resource.foo`,
   116  			map[string]cty.Value{
   117  				"null_resource": cty.ObjectVal(map[string]cty.Value{
   118  					"foo": cty.ObjectVal(map[string]cty.Value{
   119  						"attr": cty.StringVal("bar"),
   120  					}),
   121  				}),
   122  			},
   123  		},
   124  		{
   125  			`null_resource.foo.attr`,
   126  			map[string]cty.Value{
   127  				"null_resource": cty.ObjectVal(map[string]cty.Value{
   128  					"foo": cty.ObjectVal(map[string]cty.Value{
   129  						"attr": cty.StringVal("bar"),
   130  					}),
   131  				}),
   132  			},
   133  		},
   134  		{
   135  			`null_resource.multi`,
   136  			map[string]cty.Value{
   137  				"null_resource": cty.ObjectVal(map[string]cty.Value{
   138  					"multi": cty.TupleVal([]cty.Value{
   139  						cty.ObjectVal(map[string]cty.Value{
   140  							"attr": cty.StringVal("multi0"),
   141  						}),
   142  						cty.ObjectVal(map[string]cty.Value{
   143  							"attr": cty.StringVal("multi1"),
   144  						}),
   145  					}),
   146  				}),
   147  			},
   148  		},
   149  		{
   150  			// at this level, all instance references return the entire resource
   151  			`null_resource.multi[1]`,
   152  			map[string]cty.Value{
   153  				"null_resource": cty.ObjectVal(map[string]cty.Value{
   154  					"multi": cty.TupleVal([]cty.Value{
   155  						cty.ObjectVal(map[string]cty.Value{
   156  							"attr": cty.StringVal("multi0"),
   157  						}),
   158  						cty.ObjectVal(map[string]cty.Value{
   159  							"attr": cty.StringVal("multi1"),
   160  						}),
   161  					}),
   162  				}),
   163  			},
   164  		},
   165  		{
   166  			// at this level, all instance references return the entire resource
   167  			`null_resource.each["each1"]`,
   168  			map[string]cty.Value{
   169  				"null_resource": cty.ObjectVal(map[string]cty.Value{
   170  					"each": cty.ObjectVal(map[string]cty.Value{
   171  						"each0": cty.ObjectVal(map[string]cty.Value{
   172  							"attr": cty.StringVal("each0"),
   173  						}),
   174  						"each1": cty.ObjectVal(map[string]cty.Value{
   175  							"attr": cty.StringVal("each1"),
   176  						}),
   177  					}),
   178  				}),
   179  			},
   180  		},
   181  		{
   182  			`foo(null_resource.multi, null_resource.multi[1])`,
   183  			map[string]cty.Value{
   184  				"null_resource": cty.ObjectVal(map[string]cty.Value{
   185  					"multi": cty.TupleVal([]cty.Value{
   186  						cty.ObjectVal(map[string]cty.Value{
   187  							"attr": cty.StringVal("multi0"),
   188  						}),
   189  						cty.ObjectVal(map[string]cty.Value{
   190  							"attr": cty.StringVal("multi1"),
   191  						}),
   192  					}),
   193  				}),
   194  			},
   195  		},
   196  		{
   197  			`data.null_data_source.foo`,
   198  			map[string]cty.Value{
   199  				"data": cty.ObjectVal(map[string]cty.Value{
   200  					"null_data_source": cty.ObjectVal(map[string]cty.Value{
   201  						"foo": cty.ObjectVal(map[string]cty.Value{
   202  							"attr": cty.StringVal("bar"),
   203  						}),
   204  					}),
   205  				}),
   206  			},
   207  		},
   208  		{
   209  			`module.foo`,
   210  			map[string]cty.Value{
   211  				"module": cty.ObjectVal(map[string]cty.Value{
   212  					"foo": cty.ObjectVal(map[string]cty.Value{
   213  						"output0": cty.StringVal("bar0"),
   214  						"output1": cty.StringVal("bar1"),
   215  					}),
   216  				}),
   217  			},
   218  		},
   219  		{
   220  			`module.foo.output1`,
   221  			map[string]cty.Value{
   222  				"module": cty.ObjectVal(map[string]cty.Value{
   223  					"foo": cty.ObjectVal(map[string]cty.Value{
   224  						"output1": cty.StringVal("bar1"),
   225  					}),
   226  				}),
   227  			},
   228  		},
   229  		{
   230  			`path.module`,
   231  			map[string]cty.Value{
   232  				"path": cty.ObjectVal(map[string]cty.Value{
   233  					"module": cty.StringVal("foo/bar"),
   234  				}),
   235  			},
   236  		},
   237  		{
   238  			`self.baz`,
   239  			map[string]cty.Value{
   240  				"self": cty.ObjectVal(map[string]cty.Value{
   241  					"attr": cty.StringVal("multi1"),
   242  				}),
   243  			},
   244  		},
   245  		{
   246  			`terraform.workspace`,
   247  			map[string]cty.Value{
   248  				"terraform": cty.ObjectVal(map[string]cty.Value{
   249  					"workspace": cty.StringVal("default"),
   250  				}),
   251  			},
   252  		},
   253  		{
   254  			`var.baz`,
   255  			map[string]cty.Value{
   256  				"var": cty.ObjectVal(map[string]cty.Value{
   257  					"baz": cty.StringVal("boop"),
   258  				}),
   259  			},
   260  		},
   261  	}
   262  
   263  	for _, test := range tests {
   264  		t.Run(test.Expr, func(t *testing.T) {
   265  			expr, parseDiags := hclsyntax.ParseExpression([]byte(test.Expr), "", hcl.Pos{Line: 1, Column: 1})
   266  			if len(parseDiags) != 0 {
   267  				t.Errorf("unexpected diagnostics during parse")
   268  				for _, diag := range parseDiags {
   269  					t.Errorf("- %s", diag)
   270  				}
   271  				return
   272  			}
   273  
   274  			refs, refsDiags := ReferencesInExpr(expr)
   275  			if refsDiags.HasErrors() {
   276  				t.Fatal(refsDiags.Err())
   277  			}
   278  
   279  			scope := &Scope{
   280  				Data: data,
   281  
   282  				// "self" will just be an arbitrary one of the several resource
   283  				// instances we have in our test dataset.
   284  				SelfAddr: addrs.ResourceInstance{
   285  					Resource: addrs.Resource{
   286  						Mode: addrs.ManagedResourceMode,
   287  						Type: "null_resource",
   288  						Name: "multi",
   289  					},
   290  					Key: addrs.IntKey(1),
   291  				},
   292  			}
   293  			ctx, ctxDiags := scope.EvalContext(refs)
   294  			if ctxDiags.HasErrors() {
   295  				t.Fatal(ctxDiags.Err())
   296  			}
   297  
   298  			// For easier test assertions we'll just remove any top-level
   299  			// empty objects from our variables map.
   300  			for k, v := range ctx.Variables {
   301  				if v.RawEquals(cty.EmptyObjectVal) {
   302  					delete(ctx.Variables, k)
   303  				}
   304  			}
   305  
   306  			gotVal := cty.ObjectVal(ctx.Variables)
   307  			wantVal := cty.ObjectVal(test.Want)
   308  
   309  			if !gotVal.RawEquals(wantVal) {
   310  				// We'll JSON-ize our values here just so it's easier to
   311  				// read them in the assertion output.
   312  				gotJSON := formattedJSONValue(gotVal)
   313  				wantJSON := formattedJSONValue(wantVal)
   314  
   315  				t.Errorf(
   316  					"wrong result\nexpr: %s\ngot:  %s\nwant: %s",
   317  					test.Expr, gotJSON, wantJSON,
   318  				)
   319  			}
   320  		})
   321  	}
   322  }
   323  
   324  func TestScopeExpandEvalBlock(t *testing.T) {
   325  	nestedObjTy := cty.Object(map[string]cty.Type{
   326  		"boop": cty.String,
   327  	})
   328  	schema := &configschema.Block{
   329  		Attributes: map[string]*configschema.Attribute{
   330  			"foo":         {Type: cty.String, Optional: true},
   331  			"list_of_obj": {Type: cty.List(nestedObjTy), Optional: true},
   332  		},
   333  		BlockTypes: map[string]*configschema.NestedBlock{
   334  			"bar": {
   335  				Nesting: configschema.NestingMap,
   336  				Block: configschema.Block{
   337  					Attributes: map[string]*configschema.Attribute{
   338  						"baz": {Type: cty.String, Optional: true},
   339  					},
   340  				},
   341  			},
   342  		},
   343  	}
   344  	data := &dataForTests{
   345  		LocalValues: map[string]cty.Value{
   346  			"greeting": cty.StringVal("howdy"),
   347  			"list": cty.ListVal([]cty.Value{
   348  				cty.StringVal("elem0"),
   349  				cty.StringVal("elem1"),
   350  			}),
   351  			"map": cty.MapVal(map[string]cty.Value{
   352  				"key1": cty.StringVal("val1"),
   353  				"key2": cty.StringVal("val2"),
   354  			}),
   355  		},
   356  	}
   357  
   358  	tests := map[string]struct {
   359  		Config string
   360  		Want   cty.Value
   361  	}{
   362  		"empty": {
   363  			`
   364  			`,
   365  			cty.ObjectVal(map[string]cty.Value{
   366  				"foo":         cty.NullVal(cty.String),
   367  				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
   368  				"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
   369  					"baz": cty.String,
   370  				})),
   371  			}),
   372  		},
   373  		"literal attribute": {
   374  			`
   375  			foo = "hello"
   376  			`,
   377  			cty.ObjectVal(map[string]cty.Value{
   378  				"foo":         cty.StringVal("hello"),
   379  				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
   380  				"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
   381  					"baz": cty.String,
   382  				})),
   383  			}),
   384  		},
   385  		"variable attribute": {
   386  			`
   387  			foo = local.greeting
   388  			`,
   389  			cty.ObjectVal(map[string]cty.Value{
   390  				"foo":         cty.StringVal("howdy"),
   391  				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
   392  				"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
   393  					"baz": cty.String,
   394  				})),
   395  			}),
   396  		},
   397  		"one static block": {
   398  			`
   399  			bar "static" {}
   400  			`,
   401  			cty.ObjectVal(map[string]cty.Value{
   402  				"foo":         cty.NullVal(cty.String),
   403  				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
   404  				"bar": cty.MapVal(map[string]cty.Value{
   405  					"static": cty.ObjectVal(map[string]cty.Value{
   406  						"baz": cty.NullVal(cty.String),
   407  					}),
   408  				}),
   409  			}),
   410  		},
   411  		"two static blocks": {
   412  			`
   413  			bar "static0" {
   414  				baz = 0
   415  			}
   416  			bar "static1" {
   417  				baz = 1
   418  			}
   419  			`,
   420  			cty.ObjectVal(map[string]cty.Value{
   421  				"foo":         cty.NullVal(cty.String),
   422  				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
   423  				"bar": cty.MapVal(map[string]cty.Value{
   424  					"static0": cty.ObjectVal(map[string]cty.Value{
   425  						"baz": cty.StringVal("0"),
   426  					}),
   427  					"static1": cty.ObjectVal(map[string]cty.Value{
   428  						"baz": cty.StringVal("1"),
   429  					}),
   430  				}),
   431  			}),
   432  		},
   433  		"dynamic blocks from list": {
   434  			`
   435  			dynamic "bar" {
   436  				for_each = local.list
   437  				labels = [bar.value]
   438  				content {
   439  					baz = bar.key
   440  				}
   441  			}
   442  			`,
   443  			cty.ObjectVal(map[string]cty.Value{
   444  				"foo":         cty.NullVal(cty.String),
   445  				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
   446  				"bar": cty.MapVal(map[string]cty.Value{
   447  					"elem0": cty.ObjectVal(map[string]cty.Value{
   448  						"baz": cty.StringVal("0"),
   449  					}),
   450  					"elem1": cty.ObjectVal(map[string]cty.Value{
   451  						"baz": cty.StringVal("1"),
   452  					}),
   453  				}),
   454  			}),
   455  		},
   456  		"dynamic blocks from map": {
   457  			`
   458  			dynamic "bar" {
   459  				for_each = local.map
   460  				labels = [bar.key]
   461  				content {
   462  					baz = bar.value
   463  				}
   464  			}
   465  			`,
   466  			cty.ObjectVal(map[string]cty.Value{
   467  				"foo":         cty.NullVal(cty.String),
   468  				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
   469  				"bar": cty.MapVal(map[string]cty.Value{
   470  					"key1": cty.ObjectVal(map[string]cty.Value{
   471  						"baz": cty.StringVal("val1"),
   472  					}),
   473  					"key2": cty.ObjectVal(map[string]cty.Value{
   474  						"baz": cty.StringVal("val2"),
   475  					}),
   476  				}),
   477  			}),
   478  		},
   479  		"list-of-object attribute": {
   480  			`
   481  			list_of_obj = [
   482  				{
   483  					boop = local.greeting
   484  				},
   485  			]
   486  			`,
   487  			cty.ObjectVal(map[string]cty.Value{
   488  				"foo": cty.NullVal(cty.String),
   489  				"list_of_obj": cty.ListVal([]cty.Value{
   490  					cty.ObjectVal(map[string]cty.Value{
   491  						"boop": cty.StringVal("howdy"),
   492  					}),
   493  				}),
   494  				"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
   495  					"baz": cty.String,
   496  				})),
   497  			}),
   498  		},
   499  		"list-of-object attribute as blocks": {
   500  			`
   501  			list_of_obj {
   502  				boop = local.greeting
   503  			}
   504  			`,
   505  			cty.ObjectVal(map[string]cty.Value{
   506  				"foo": cty.NullVal(cty.String),
   507  				"list_of_obj": cty.ListVal([]cty.Value{
   508  					cty.ObjectVal(map[string]cty.Value{
   509  						"boop": cty.StringVal("howdy"),
   510  					}),
   511  				}),
   512  				"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
   513  					"baz": cty.String,
   514  				})),
   515  			}),
   516  		},
   517  		"lots of things at once": {
   518  			`
   519  			foo = "whoop"
   520  			bar "static0" {
   521  				baz = "s0"
   522  			}
   523  			dynamic "bar" {
   524  				for_each = local.list
   525  				labels = [bar.value]
   526  				content {
   527  					baz = bar.key
   528  				}
   529  			}
   530  			bar "static1" {
   531  				baz = "s1"
   532  			}
   533  			dynamic "bar" {
   534  				for_each = local.map
   535  				labels = [bar.key]
   536  				content {
   537  					baz = bar.value
   538  				}
   539  			}
   540  			bar "static2" {
   541  				baz = "s2"
   542  			}
   543  			`,
   544  			cty.ObjectVal(map[string]cty.Value{
   545  				"foo":         cty.StringVal("whoop"),
   546  				"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
   547  				"bar": cty.MapVal(map[string]cty.Value{
   548  					"key1": cty.ObjectVal(map[string]cty.Value{
   549  						"baz": cty.StringVal("val1"),
   550  					}),
   551  					"key2": cty.ObjectVal(map[string]cty.Value{
   552  						"baz": cty.StringVal("val2"),
   553  					}),
   554  					"elem0": cty.ObjectVal(map[string]cty.Value{
   555  						"baz": cty.StringVal("0"),
   556  					}),
   557  					"elem1": cty.ObjectVal(map[string]cty.Value{
   558  						"baz": cty.StringVal("1"),
   559  					}),
   560  					"static0": cty.ObjectVal(map[string]cty.Value{
   561  						"baz": cty.StringVal("s0"),
   562  					}),
   563  					"static1": cty.ObjectVal(map[string]cty.Value{
   564  						"baz": cty.StringVal("s1"),
   565  					}),
   566  					"static2": cty.ObjectVal(map[string]cty.Value{
   567  						"baz": cty.StringVal("s2"),
   568  					}),
   569  				}),
   570  			}),
   571  		},
   572  	}
   573  
   574  	for name, test := range tests {
   575  		t.Run(name, func(t *testing.T) {
   576  			file, parseDiags := hclsyntax.ParseConfig([]byte(test.Config), "", hcl.Pos{Line: 1, Column: 1})
   577  			if len(parseDiags) != 0 {
   578  				t.Errorf("unexpected diagnostics during parse")
   579  				for _, diag := range parseDiags {
   580  					t.Errorf("- %s", diag)
   581  				}
   582  				return
   583  			}
   584  
   585  			body := file.Body
   586  			scope := &Scope{
   587  				Data: data,
   588  			}
   589  
   590  			body, expandDiags := scope.ExpandBlock(body, schema)
   591  			if expandDiags.HasErrors() {
   592  				t.Fatal(expandDiags.Err())
   593  			}
   594  
   595  			got, valDiags := scope.EvalBlock(body, schema)
   596  			if valDiags.HasErrors() {
   597  				t.Fatal(valDiags.Err())
   598  			}
   599  
   600  			if !got.RawEquals(test.Want) {
   601  				// We'll JSON-ize our values here just so it's easier to
   602  				// read them in the assertion output.
   603  				gotJSON := formattedJSONValue(got)
   604  				wantJSON := formattedJSONValue(test.Want)
   605  
   606  				t.Errorf(
   607  					"wrong result\nconfig: %s\ngot:   %s\nwant:  %s",
   608  					test.Config, gotJSON, wantJSON,
   609  				)
   610  			}
   611  
   612  		})
   613  	}
   614  
   615  }
   616  
   617  func formattedJSONValue(val cty.Value) string {
   618  	val = cty.UnknownAsNull(val) // since JSON can't represent unknowns
   619  	j, err := ctyjson.Marshal(val, val.Type())
   620  	if err != nil {
   621  		panic(err)
   622  	}
   623  	var buf bytes.Buffer
   624  	json.Indent(&buf, j, "", "  ")
   625  	return buf.String()
   626  }