opentofu/internal/lang/eval_test.go
Martin Atkins cdd9464f9a Move lang/ to internal/lang/
This is part of a general effort to move all of Terraform's non-library
package surface under internal in order to reinforce that these are for
internal use within Terraform only.

If you were previously importing packages under this prefix into an
external codebase, you could pin to an earlier release tag as an interim
solution until you've make a plan to achieve the same functionality some
other way.
2021-05-17 14:09:07 -07:00

845 lines
20 KiB
Go

package lang
import (
"bytes"
"encoding/json"
"testing"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
)
func TestScopeEvalContext(t *testing.T) {
data := &dataForTests{
CountAttrs: map[string]cty.Value{
"index": cty.NumberIntVal(0),
},
ForEachAttrs: map[string]cty.Value{
"key": cty.StringVal("a"),
"value": cty.NumberIntVal(1),
},
Resources: map[string]cty.Value{
"null_resource.foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
}),
"data.null_data_source.foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
}),
"null_resource.multi": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi0"),
}),
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
}),
"null_resource.each": cty.ObjectVal(map[string]cty.Value{
"each0": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("each0"),
}),
"each1": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("each1"),
}),
}),
"null_resource.multi[1]": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
},
LocalValues: map[string]cty.Value{
"foo": cty.StringVal("bar"),
},
Modules: map[string]cty.Value{
"module.foo": cty.ObjectVal(map[string]cty.Value{
"output0": cty.StringVal("bar0"),
"output1": cty.StringVal("bar1"),
}),
},
PathAttrs: map[string]cty.Value{
"module": cty.StringVal("foo/bar"),
},
TerraformAttrs: map[string]cty.Value{
"workspace": cty.StringVal("default"),
},
InputVariables: map[string]cty.Value{
"baz": cty.StringVal("boop"),
},
}
tests := []struct {
Expr string
Want map[string]cty.Value
}{
{
`12`,
map[string]cty.Value{},
},
{
`count.index`,
map[string]cty.Value{
"count": cty.ObjectVal(map[string]cty.Value{
"index": cty.NumberIntVal(0),
}),
},
},
{
`each.key`,
map[string]cty.Value{
"each": cty.ObjectVal(map[string]cty.Value{
"key": cty.StringVal("a"),
}),
},
},
{
`each.value`,
map[string]cty.Value{
"each": cty.ObjectVal(map[string]cty.Value{
"value": cty.NumberIntVal(1),
}),
},
},
{
`local.foo`,
map[string]cty.Value{
"local": cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
}),
},
},
{
`null_resource.foo`,
map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
}),
}),
"resource": cty.ObjectVal(map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
}),
}),
}),
},
},
{
`null_resource.foo.attr`,
map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
}),
}),
"resource": cty.ObjectVal(map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
}),
}),
}),
},
},
{
`null_resource.multi`,
map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"multi": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi0"),
}),
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
}),
}),
"resource": cty.ObjectVal(map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"multi": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi0"),
}),
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
}),
}),
}),
},
},
{
// at this level, all instance references return the entire resource
`null_resource.multi[1]`,
map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"multi": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi0"),
}),
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
}),
}),
"resource": cty.ObjectVal(map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"multi": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi0"),
}),
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
}),
}),
}),
},
},
{
// at this level, all instance references return the entire resource
`null_resource.each["each1"]`,
map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"each": cty.ObjectVal(map[string]cty.Value{
"each0": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("each0"),
}),
"each1": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("each1"),
}),
}),
}),
"resource": cty.ObjectVal(map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"each": cty.ObjectVal(map[string]cty.Value{
"each0": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("each0"),
}),
"each1": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("each1"),
}),
}),
}),
}),
},
},
{
// at this level, all instance references return the entire resource
`null_resource.each["each1"].attr`,
map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"each": cty.ObjectVal(map[string]cty.Value{
"each0": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("each0"),
}),
"each1": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("each1"),
}),
}),
}),
"resource": cty.ObjectVal(map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"each": cty.ObjectVal(map[string]cty.Value{
"each0": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("each0"),
}),
"each1": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("each1"),
}),
}),
}),
}),
},
},
{
`foo(null_resource.multi, null_resource.multi[1])`,
map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"multi": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi0"),
}),
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
}),
}),
"resource": cty.ObjectVal(map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"multi": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi0"),
}),
cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
}),
}),
}),
},
},
{
`data.null_data_source.foo`,
map[string]cty.Value{
"data": cty.ObjectVal(map[string]cty.Value{
"null_data_source": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
}),
}),
}),
},
},
{
`module.foo`,
map[string]cty.Value{
"module": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"output0": cty.StringVal("bar0"),
"output1": cty.StringVal("bar1"),
}),
}),
},
},
// any module reference returns the entire module
{
`module.foo.output1`,
map[string]cty.Value{
"module": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"output0": cty.StringVal("bar0"),
"output1": cty.StringVal("bar1"),
}),
}),
},
},
{
`path.module`,
map[string]cty.Value{
"path": cty.ObjectVal(map[string]cty.Value{
"module": cty.StringVal("foo/bar"),
}),
},
},
{
`self.baz`,
map[string]cty.Value{
"self": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
},
},
{
`terraform.workspace`,
map[string]cty.Value{
"terraform": cty.ObjectVal(map[string]cty.Value{
"workspace": cty.StringVal("default"),
}),
},
},
{
`var.baz`,
map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("boop"),
}),
},
},
}
for _, test := range tests {
t.Run(test.Expr, func(t *testing.T) {
expr, parseDiags := hclsyntax.ParseExpression([]byte(test.Expr), "", hcl.Pos{Line: 1, Column: 1})
if len(parseDiags) != 0 {
t.Errorf("unexpected diagnostics during parse")
for _, diag := range parseDiags {
t.Errorf("- %s", diag)
}
return
}
refs, refsDiags := ReferencesInExpr(expr)
if refsDiags.HasErrors() {
t.Fatal(refsDiags.Err())
}
scope := &Scope{
Data: data,
// "self" will just be an arbitrary one of the several resource
// instances we have in our test dataset.
SelfAddr: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "null_resource",
Name: "multi",
},
Key: addrs.IntKey(1),
},
}
ctx, ctxDiags := scope.EvalContext(refs)
if ctxDiags.HasErrors() {
t.Fatal(ctxDiags.Err())
}
// For easier test assertions we'll just remove any top-level
// empty objects from our variables map.
for k, v := range ctx.Variables {
if v.RawEquals(cty.EmptyObjectVal) {
delete(ctx.Variables, k)
}
}
gotVal := cty.ObjectVal(ctx.Variables)
wantVal := cty.ObjectVal(test.Want)
if !gotVal.RawEquals(wantVal) {
// We'll JSON-ize our values here just so it's easier to
// read them in the assertion output.
gotJSON := formattedJSONValue(gotVal)
wantJSON := formattedJSONValue(wantVal)
t.Errorf(
"wrong result\nexpr: %s\ngot: %s\nwant: %s",
test.Expr, gotJSON, wantJSON,
)
}
})
}
}
func TestScopeExpandEvalBlock(t *testing.T) {
nestedObjTy := cty.Object(map[string]cty.Type{
"boop": cty.String,
})
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {Type: cty.String, Optional: true},
"list_of_obj": {Type: cty.List(nestedObjTy), Optional: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"bar": {
Nesting: configschema.NestingMap,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"baz": {Type: cty.String, Optional: true},
},
},
},
},
}
data := &dataForTests{
LocalValues: map[string]cty.Value{
"greeting": cty.StringVal("howdy"),
"list": cty.ListVal([]cty.Value{
cty.StringVal("elem0"),
cty.StringVal("elem1"),
}),
"map": cty.MapVal(map[string]cty.Value{
"key1": cty.StringVal("val1"),
"key2": cty.StringVal("val2"),
}),
},
}
tests := map[string]struct {
Config string
Want cty.Value
}{
"empty": {
`
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
"baz": cty.String,
})),
}),
},
"literal attribute": {
`
foo = "hello"
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("hello"),
"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
"baz": cty.String,
})),
}),
},
"variable attribute": {
`
foo = local.greeting
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("howdy"),
"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
"baz": cty.String,
})),
}),
},
"one static block": {
`
bar "static" {}
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
"bar": cty.MapVal(map[string]cty.Value{
"static": cty.ObjectVal(map[string]cty.Value{
"baz": cty.NullVal(cty.String),
}),
}),
}),
},
"two static blocks": {
`
bar "static0" {
baz = 0
}
bar "static1" {
baz = 1
}
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
"bar": cty.MapVal(map[string]cty.Value{
"static0": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("0"),
}),
"static1": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("1"),
}),
}),
}),
},
"dynamic blocks from list": {
`
dynamic "bar" {
for_each = local.list
labels = [bar.value]
content {
baz = bar.key
}
}
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
"bar": cty.MapVal(map[string]cty.Value{
"elem0": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("0"),
}),
"elem1": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("1"),
}),
}),
}),
},
"dynamic blocks from map": {
`
dynamic "bar" {
for_each = local.map
labels = [bar.key]
content {
baz = bar.value
}
}
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
"bar": cty.MapVal(map[string]cty.Value{
"key1": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("val1"),
}),
"key2": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("val2"),
}),
}),
}),
},
"list-of-object attribute": {
`
list_of_obj = [
{
boop = local.greeting
},
]
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
"list_of_obj": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"boop": cty.StringVal("howdy"),
}),
}),
"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
"baz": cty.String,
})),
}),
},
"list-of-object attribute as blocks": {
`
list_of_obj {
boop = local.greeting
}
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
"list_of_obj": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"boop": cty.StringVal("howdy"),
}),
}),
"bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{
"baz": cty.String,
})),
}),
},
"lots of things at once": {
`
foo = "whoop"
bar "static0" {
baz = "s0"
}
dynamic "bar" {
for_each = local.list
labels = [bar.value]
content {
baz = bar.key
}
}
bar "static1" {
baz = "s1"
}
dynamic "bar" {
for_each = local.map
labels = [bar.key]
content {
baz = bar.value
}
}
bar "static2" {
baz = "s2"
}
`,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("whoop"),
"list_of_obj": cty.NullVal(cty.List(nestedObjTy)),
"bar": cty.MapVal(map[string]cty.Value{
"key1": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("val1"),
}),
"key2": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("val2"),
}),
"elem0": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("0"),
}),
"elem1": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("1"),
}),
"static0": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("s0"),
}),
"static1": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("s1"),
}),
"static2": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("s2"),
}),
}),
}),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
file, parseDiags := hclsyntax.ParseConfig([]byte(test.Config), "", hcl.Pos{Line: 1, Column: 1})
if len(parseDiags) != 0 {
t.Errorf("unexpected diagnostics during parse")
for _, diag := range parseDiags {
t.Errorf("- %s", diag)
}
return
}
body := file.Body
scope := &Scope{
Data: data,
}
body, expandDiags := scope.ExpandBlock(body, schema)
if expandDiags.HasErrors() {
t.Fatal(expandDiags.Err())
}
got, valDiags := scope.EvalBlock(body, schema)
if valDiags.HasErrors() {
t.Fatal(valDiags.Err())
}
if !got.RawEquals(test.Want) {
// We'll JSON-ize our values here just so it's easier to
// read them in the assertion output.
gotJSON := formattedJSONValue(got)
wantJSON := formattedJSONValue(test.Want)
t.Errorf(
"wrong result\nconfig: %s\ngot: %s\nwant: %s",
test.Config, gotJSON, wantJSON,
)
}
})
}
}
func formattedJSONValue(val cty.Value) string {
val = cty.UnknownAsNull(val) // since JSON can't represent unknowns
j, err := ctyjson.Marshal(val, val.Type())
if err != nil {
panic(err)
}
var buf bytes.Buffer
json.Indent(&buf, j, "", " ")
return buf.String()
}
func TestScopeEvalSelfBlock(t *testing.T) {
data := &dataForTests{
PathAttrs: map[string]cty.Value{
"module": cty.StringVal("foo/bar"),
"cwd": cty.StringVal("/home/foo/bar"),
"root": cty.StringVal("/home/foo"),
},
TerraformAttrs: map[string]cty.Value{
"workspace": cty.StringVal("default"),
},
}
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"attr": {
Type: cty.String,
},
"num": {
Type: cty.Number,
},
},
}
tests := []struct {
Config string
Self cty.Value
KeyData instances.RepetitionData
Want map[string]cty.Value
}{
{
Config: `attr = self.foo`,
Self: cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
}),
KeyData: instances.RepetitionData{
CountIndex: cty.NumberIntVal(0),
},
Want: map[string]cty.Value{
"attr": cty.StringVal("bar"),
"num": cty.NullVal(cty.Number),
},
},
{
Config: `num = count.index`,
KeyData: instances.RepetitionData{
CountIndex: cty.NumberIntVal(0),
},
Want: map[string]cty.Value{
"attr": cty.NullVal(cty.String),
"num": cty.NumberIntVal(0),
},
},
{
Config: `attr = each.key`,
KeyData: instances.RepetitionData{
EachKey: cty.StringVal("a"),
},
Want: map[string]cty.Value{
"attr": cty.StringVal("a"),
"num": cty.NullVal(cty.Number),
},
},
{
Config: `attr = path.cwd`,
Want: map[string]cty.Value{
"attr": cty.StringVal("/home/foo/bar"),
"num": cty.NullVal(cty.Number),
},
},
{
Config: `attr = path.module`,
Want: map[string]cty.Value{
"attr": cty.StringVal("foo/bar"),
"num": cty.NullVal(cty.Number),
},
},
{
Config: `attr = path.root`,
Want: map[string]cty.Value{
"attr": cty.StringVal("/home/foo"),
"num": cty.NullVal(cty.Number),
},
},
{
Config: `attr = terraform.workspace`,
Want: map[string]cty.Value{
"attr": cty.StringVal("default"),
"num": cty.NullVal(cty.Number),
},
},
}
for _, test := range tests {
t.Run(test.Config, func(t *testing.T) {
file, parseDiags := hclsyntax.ParseConfig([]byte(test.Config), "", hcl.Pos{Line: 1, Column: 1})
if len(parseDiags) != 0 {
t.Errorf("unexpected diagnostics during parse")
for _, diag := range parseDiags {
t.Errorf("- %s", diag)
}
return
}
body := file.Body
scope := &Scope{
Data: data,
}
gotVal, ctxDiags := scope.EvalSelfBlock(body, test.Self, schema, test.KeyData)
if ctxDiags.HasErrors() {
t.Fatal(ctxDiags.Err())
}
wantVal := cty.ObjectVal(test.Want)
if !gotVal.RawEquals(wantVal) {
t.Errorf(
"wrong result\nexpr: %s\ngot: %#v\nwant: %#v",
test.Config, gotVal, wantVal,
)
}
})
}
}