mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-23 23:50:12 -06:00
2c5c8a5f72
Signed-off-by: Christian Mesh <christianmesh1@gmail.com> Signed-off-by: James Humphries <james@james-humphries.co.uk> Co-authored-by: James Humphries <james@james-humphries.co.uk>
398 lines
12 KiB
Go
398 lines
12 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package configs
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/opentofu/opentofu/internal/configs/configschema"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
// This exercises most of the logic in StaticEvaluator and staticScopeData
|
|
//
|
|
//nolint:cyclop // it's a test
|
|
func TestStaticEvaluator_Evaluate(t *testing.T) {
|
|
// Synthetic file for building test components
|
|
testData := `
|
|
variable "str" {
|
|
type = string
|
|
}
|
|
variable "str_map" {
|
|
type = map(string)
|
|
}
|
|
variable "str_def" {
|
|
type = string
|
|
default = "sane default"
|
|
}
|
|
variable "str_map_def" {
|
|
type = map(string)
|
|
default = {
|
|
keyA = "A"
|
|
}
|
|
}
|
|
# Should/can variable checks be performed during static evaluation?
|
|
|
|
locals {
|
|
# Simple static cases
|
|
static = "static"
|
|
static_ref = local.static
|
|
static_fn = md5(local.static)
|
|
path_root = path.root
|
|
path_module = path.module
|
|
|
|
# Variable References with Defaults
|
|
var_str_def_ref = var.str_def
|
|
var_map_def_access = var.str_map_def["keyA"]
|
|
|
|
# Variable References without Defaults
|
|
var_str_ref = var.str
|
|
var_map_access = var.str_map["keyA"]
|
|
|
|
# Bad References
|
|
invalid_ref = invalid.attribute
|
|
unavailable_ref = foo.bar.attribute
|
|
|
|
# Circular References
|
|
circular = local.circular
|
|
circular_ref = local.circular
|
|
circular_a = local.circular_b
|
|
circular_b = local.circular_a
|
|
|
|
# Dependency chain
|
|
ref_a = var.str
|
|
ref_b = local.ref_a
|
|
ref_c = local.ref_b
|
|
|
|
# Missing References
|
|
local_missing = local.missing
|
|
var_missing = var.missing
|
|
|
|
# Terraform
|
|
ws = terraform.workspace
|
|
|
|
# Functions
|
|
func = md5("my-string")
|
|
missing_func = missing_fn("my-string")
|
|
provider_func = provider::type::fn("my-string")
|
|
}
|
|
|
|
resource "foo" "bar" {}
|
|
`
|
|
|
|
parser := testParser(map[string]string{"eval.tf": testData})
|
|
|
|
file, fileDiags := parser.LoadConfigFile("eval.tf")
|
|
if fileDiags.HasErrors() {
|
|
t.Fatal(fileDiags)
|
|
}
|
|
|
|
dummyIdentifier := StaticIdentifier{Subject: "local.test"}
|
|
|
|
t.Run("Empty Eval", func(t *testing.T) {
|
|
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir", SelectiveLoadAll)
|
|
emptyEval := StaticEvaluator{}
|
|
|
|
// Expr with no traversals shouldn't access any fields
|
|
value, diags := emptyEval.Evaluate(mod.Locals["static"].Expr, dummyIdentifier)
|
|
if diags.HasErrors() {
|
|
t.Error(diags)
|
|
}
|
|
if value.AsString() != "static" {
|
|
t.Errorf("Expected %s got %s", "static", value.AsString())
|
|
}
|
|
|
|
// Expr with traversals should panic, indicating a programming error
|
|
defer func() {
|
|
r := recover()
|
|
if r == nil {
|
|
t.Fatalf("should panic")
|
|
}
|
|
}()
|
|
_, _ = emptyEval.Evaluate(mod.Locals["static_ref"].Expr, dummyIdentifier)
|
|
})
|
|
|
|
t.Run("Simple static cases", func(t *testing.T) {
|
|
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir", SelectiveLoadAll)
|
|
eval := NewStaticEvaluator(mod, RootModuleCallForTesting())
|
|
|
|
locals := []struct {
|
|
ident string
|
|
value string
|
|
}{
|
|
{"static", "static"},
|
|
{"static_ref", "static"},
|
|
{"static_fn", "a81259cef8e959c624df1d456e5d3297"},
|
|
{"path_root", "<testing>"},
|
|
{"path_module", "dir"},
|
|
}
|
|
for _, local := range locals {
|
|
t.Run(local.ident, func(t *testing.T) {
|
|
value, diags := eval.Evaluate(mod.Locals[local.ident].Expr, dummyIdentifier)
|
|
if diags.HasErrors() {
|
|
t.Error(diags)
|
|
}
|
|
if value.AsString() != local.value {
|
|
t.Errorf("Expected %s got %s", local.value, value.AsString())
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Valid Variables", func(t *testing.T) {
|
|
input := map[string]cty.Value{
|
|
"str": cty.StringVal("vara"),
|
|
"str_map": cty.MapVal(map[string]cty.Value{"keyA": cty.StringVal("mapa")}),
|
|
}
|
|
call := NewStaticModuleCall(nil, func(v *Variable) (cty.Value, hcl.Diagnostics) {
|
|
if in, ok := input[v.Name]; ok {
|
|
return in, nil
|
|
}
|
|
return v.Default, nil
|
|
}, "<testing>", "")
|
|
mod, _ := NewModule([]*File{file}, nil, call, "dir", SelectiveLoadAll)
|
|
eval := NewStaticEvaluator(mod, call)
|
|
|
|
locals := []struct {
|
|
ident string
|
|
value string
|
|
}{
|
|
{"var_str_def_ref", "sane default"},
|
|
{"var_map_def_access", "A"},
|
|
{"var_str_ref", "vara"},
|
|
{"var_map_access", "mapa"},
|
|
}
|
|
for _, local := range locals {
|
|
t.Run(local.ident, func(t *testing.T) {
|
|
value, diags := eval.Evaluate(mod.Locals[local.ident].Expr, dummyIdentifier)
|
|
if diags.HasErrors() {
|
|
t.Error(diags)
|
|
}
|
|
if value.AsString() != local.value {
|
|
t.Errorf("Expected %s got %s", local.value, value.AsString())
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Bad References", func(t *testing.T) {
|
|
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir", SelectiveLoadAll)
|
|
eval := NewStaticEvaluator(mod, RootModuleCallForTesting())
|
|
|
|
locals := []struct {
|
|
ident string
|
|
diag string
|
|
}{
|
|
{"invalid_ref", "eval.tf:37,16-33: Dynamic value in static context; Unable to use invalid.attribute in static context, which is required by local.invalid_ref"},
|
|
{"unavailable_ref", "eval.tf:38,20-27: Dynamic value in static context; Unable to use foo.bar in static context, which is required by local.unavailable_ref"},
|
|
}
|
|
for _, local := range locals {
|
|
t.Run(local.ident, func(t *testing.T) {
|
|
badref := mod.Locals[local.ident]
|
|
_, diags := eval.Evaluate(badref.Expr, StaticIdentifier{Subject: fmt.Sprintf("local.%s", badref.Name), DeclRange: badref.DeclRange})
|
|
assertExactDiagnostics(t, diags, []string{local.diag})
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Circular References", func(t *testing.T) {
|
|
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir", SelectiveLoadAll)
|
|
eval := NewStaticEvaluator(mod, RootModuleCallForTesting())
|
|
|
|
locals := []struct {
|
|
ident string
|
|
diags []string
|
|
}{
|
|
{"circular", []string{
|
|
"eval.tf:41,2-27: Circular reference; local.circular is self referential",
|
|
}},
|
|
{"circular_ref", []string{
|
|
"eval.tf:41,2-27: Circular reference; local.circular is self referential",
|
|
"eval.tf:42,2-31: Unable to compute static value; local.circular_ref depends on local.circular which is not available",
|
|
}},
|
|
{"circular_a", []string{
|
|
"eval.tf:43,2-31: Unable to compute static value; local.circular_a depends on local.circular_b which is not available",
|
|
"eval.tf:43,2-31: Circular reference; local.circular_a is self referential",
|
|
}},
|
|
}
|
|
for _, local := range locals {
|
|
t.Run(local.ident, func(t *testing.T) {
|
|
badref := mod.Locals[local.ident]
|
|
_, diags := eval.Evaluate(badref.Expr, StaticIdentifier{Subject: fmt.Sprintf("local.%s", badref.Name), DeclRange: badref.DeclRange})
|
|
assertExactDiagnostics(t, diags, local.diags)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Dependency chain", func(t *testing.T) {
|
|
call := NewStaticModuleCall(nil, func(v *Variable) (cty.Value, hcl.Diagnostics) {
|
|
return cty.DynamicVal, hcl.Diagnostics{&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Variable value not provided",
|
|
Detail: fmt.Sprintf("var.%s not included", v.Name),
|
|
Subject: v.DeclRange.Ptr(),
|
|
}}
|
|
}, "<testing>", "")
|
|
mod, _ := NewModule([]*File{file}, nil, call, "dir", SelectiveLoadAll)
|
|
eval := NewStaticEvaluator(mod, call)
|
|
|
|
badref := mod.Locals["ref_c"]
|
|
_, diags := eval.Evaluate(badref.Expr, StaticIdentifier{Subject: fmt.Sprintf("local.%s", badref.Name), DeclRange: badref.DeclRange})
|
|
assertExactDiagnostics(t, diags, []string{
|
|
"eval.tf:2,1-15: Variable value not provided; var.str not included",
|
|
"eval.tf:47,2-17: Unable to compute static value; local.ref_a depends on var.str which is not available",
|
|
"eval.tf:48,2-21: Unable to compute static value; local.ref_b depends on local.ref_a which is not available",
|
|
"eval.tf:49,2-21: Unable to compute static value; local.ref_c depends on local.ref_b which is not available",
|
|
})
|
|
})
|
|
|
|
t.Run("Missing References", func(t *testing.T) {
|
|
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir", SelectiveLoadAll)
|
|
eval := NewStaticEvaluator(mod, RootModuleCallForTesting())
|
|
|
|
locals := []struct {
|
|
ident string
|
|
diag string
|
|
}{
|
|
{"local_missing", "eval.tf:52,18-31: Undefined local; Undefined local local.missing"},
|
|
{"var_missing", "eval.tf:53,16-27: Undefined variable; Undefined variable var.missing"},
|
|
}
|
|
for _, local := range locals {
|
|
t.Run(local.ident, func(t *testing.T) {
|
|
badref := mod.Locals[local.ident]
|
|
_, diags := eval.Evaluate(badref.Expr, StaticIdentifier{Subject: fmt.Sprintf("local.%s", badref.Name), DeclRange: badref.DeclRange})
|
|
assertExactDiagnostics(t, diags, []string{local.diag})
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Workspace", func(t *testing.T) {
|
|
call := NewStaticModuleCall(nil, nil, "<testing>", "my-workspace")
|
|
mod, _ := NewModule([]*File{file}, nil, call, "dir", SelectiveLoadAll)
|
|
eval := NewStaticEvaluator(mod, call)
|
|
|
|
value, diags := eval.Evaluate(mod.Locals["ws"].Expr, dummyIdentifier)
|
|
if diags.HasErrors() {
|
|
t.Error(diags)
|
|
}
|
|
if value.AsString() != "my-workspace" {
|
|
t.Errorf("Expected %s got %s", "my-workspace", value.AsString())
|
|
}
|
|
})
|
|
|
|
t.Run("Functions", func(t *testing.T) {
|
|
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir", SelectiveLoadAll)
|
|
eval := NewStaticEvaluator(mod, RootModuleCallForTesting())
|
|
|
|
value, diags := eval.Evaluate(mod.Locals["func"].Expr, dummyIdentifier)
|
|
if diags.HasErrors() {
|
|
t.Error(diags)
|
|
}
|
|
if value.AsString() != "f887f41a53a46e2d40a3f8f86cacaaa2" {
|
|
t.Errorf("Expected %s got %s", "f887f41a53a46e2d40a3f8f86cacaaa2", value.AsString())
|
|
}
|
|
|
|
_, diags = eval.Evaluate(mod.Locals["missing_func"].Expr, StaticIdentifier{Subject: fmt.Sprintf("local.%s", mod.Locals["missing_func"].Name), DeclRange: mod.Locals["missing_func"].DeclRange})
|
|
assertExactDiagnostics(t, diags, []string{`eval.tf:60,17-27: Call to unknown function; There is no function named "missing_fn".`})
|
|
_, diags = eval.Evaluate(mod.Locals["provider_func"].Expr, StaticIdentifier{Subject: fmt.Sprintf("local.%s", mod.Locals["provider_func"].Name), DeclRange: mod.Locals["provider_func"].DeclRange})
|
|
assertExactDiagnostics(t, diags, []string{`eval.tf:61,18-36: Provider function in static context; Unable to use provider::type::fn in static context, which is required by local.provider_func`})
|
|
})
|
|
}
|
|
|
|
func TestStaticEvaluator_DecodeExpression(t *testing.T) {
|
|
dummyIdentifier := StaticIdentifier{Subject: "local.test"}
|
|
parser := testParser(map[string]string{"eval.tf": ""})
|
|
file, fileDiags := parser.LoadConfigFile("eval.tf")
|
|
if fileDiags.HasErrors() {
|
|
t.Fatal(fileDiags)
|
|
}
|
|
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir", SelectiveLoadAll)
|
|
eval := NewStaticEvaluator(mod, RootModuleCallForTesting())
|
|
|
|
cases := []struct {
|
|
expr string
|
|
diags []string
|
|
}{{
|
|
expr: `"static"`,
|
|
}, {
|
|
expr: `count`,
|
|
diags: []string{`eval.tf:1,1-6: Invalid reference; The "count" object cannot be accessed directly. Instead, access one of its attributes.`},
|
|
}, {
|
|
expr: `module.foo.bar`,
|
|
diags: []string{`eval.tf:1,1-15: Module output not supported in static context; Unable to use module.foo.bar in static context, which is required by local.test`},
|
|
}}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.expr, func(t *testing.T) {
|
|
expr, _ := hclsyntax.ParseExpression([]byte(tc.expr), "eval.tf", hcl.InitialPos)
|
|
var str string
|
|
diags := eval.DecodeExpression(expr, dummyIdentifier, &str)
|
|
assertExactDiagnostics(t, diags, tc.diags)
|
|
})
|
|
}
|
|
}
|
|
func TestStaticEvaluator_DecodeBlock(t *testing.T) {
|
|
cases := []struct {
|
|
ident string
|
|
body string
|
|
diags []string
|
|
}{{
|
|
ident: "valid",
|
|
body: `
|
|
locals {
|
|
static = "static"
|
|
}
|
|
|
|
terraform {
|
|
backend "valid" {
|
|
thing = local.static
|
|
}
|
|
}`,
|
|
}, {
|
|
ident: "badref",
|
|
body: `
|
|
terraform {
|
|
backend "badref" {
|
|
thing = count
|
|
}
|
|
}`,
|
|
diags: []string{`eval.tf:4,11-16: Invalid reference; The "count" object cannot be accessed directly. Instead, access one of its attributes.`},
|
|
}, {
|
|
ident: "badeval",
|
|
body: `
|
|
terraform {
|
|
backend "badeval" {
|
|
thing = module.foo.bar
|
|
}
|
|
}`,
|
|
diags: []string{`eval.tf:4,11-25: Module output not supported in static context; Unable to use module.foo.bar in static context, which is required by backend.badeval`},
|
|
}}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.ident, func(t *testing.T) {
|
|
parser := testParser(map[string]string{"eval.tf": tc.body})
|
|
file, fileDiags := parser.LoadConfigFile("eval.tf")
|
|
if fileDiags.HasErrors() {
|
|
t.Fatal(fileDiags)
|
|
}
|
|
|
|
mod, _ := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "dir", SelectiveLoadAll)
|
|
_, diags := mod.Backend.Decode(&configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"thing": &configschema.Attribute{
|
|
Type: cty.String,
|
|
},
|
|
},
|
|
})
|
|
|
|
assertExactDiagnostics(t, diags, tc.diags)
|
|
})
|
|
}
|
|
}
|