2024-02-08 03:48:59 -06:00
|
|
|
// Copyright (c) The OpenTofu Authors
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
2023-05-02 10:33:06 -05:00
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2023-09-20 07:16:53 -05:00
|
|
|
package tofu
|
2016-09-16 16:18:43 -05:00
|
|
|
|
|
|
|
import (
|
2023-11-08 17:07:09 -06:00
|
|
|
"errors"
|
2016-09-16 16:18:43 -05:00
|
|
|
"reflect"
|
|
|
|
"testing"
|
|
|
|
|
2018-05-10 17:59:47 -05:00
|
|
|
"github.com/go-test/deep"
|
2019-09-09 17:58:44 -05:00
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
2021-09-10 09:58:44 -05:00
|
|
|
"github.com/zclconf/go-cty/cty"
|
2018-05-04 21:24:06 -05:00
|
|
|
|
2023-09-20 06:35:35 -05:00
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
2023-11-08 17:07:09 -06:00
|
|
|
"github.com/opentofu/opentofu/internal/checks"
|
2023-09-20 06:35:35 -05:00
|
|
|
"github.com/opentofu/opentofu/internal/configs"
|
2023-11-08 17:07:09 -06:00
|
|
|
"github.com/opentofu/opentofu/internal/configs/configschema"
|
|
|
|
"github.com/opentofu/opentofu/internal/plans"
|
|
|
|
"github.com/opentofu/opentofu/internal/providers"
|
|
|
|
"github.com/opentofu/opentofu/internal/states"
|
|
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
2016-09-16 16:18:43 -05:00
|
|
|
)
|
|
|
|
|
2020-10-14 08:10:37 -05:00
|
|
|
func TestNodeModuleVariablePath(t *testing.T) {
|
2020-04-07 16:11:32 -05:00
|
|
|
n := &nodeModuleVariable{
|
|
|
|
Addr: addrs.RootModuleInstance.InputVariable("foo"),
|
2018-05-04 21:24:06 -05:00
|
|
|
Config: &configs.Variable{
|
2021-09-10 09:58:44 -05:00
|
|
|
Name: "foo",
|
|
|
|
Type: cty.String,
|
|
|
|
ConstraintType: cty.String,
|
2018-05-04 21:24:06 -05:00
|
|
|
},
|
2016-09-16 16:18:43 -05:00
|
|
|
}
|
|
|
|
|
2018-05-10 18:20:34 -05:00
|
|
|
want := addrs.RootModuleInstance
|
|
|
|
got := n.Path()
|
|
|
|
if got.String() != want.String() {
|
|
|
|
t.Fatalf("wrong module address %s; want %s", got, want)
|
2016-09-16 16:18:43 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-14 08:10:37 -05:00
|
|
|
func TestNodeModuleVariableReferenceableName(t *testing.T) {
|
2020-04-07 16:11:32 -05:00
|
|
|
n := &nodeExpandModuleVariable{
|
|
|
|
Addr: addrs.InputVariable{Name: "foo"},
|
2018-05-04 21:24:06 -05:00
|
|
|
Config: &configs.Variable{
|
2021-09-10 09:58:44 -05:00
|
|
|
Name: "foo",
|
|
|
|
Type: cty.String,
|
|
|
|
ConstraintType: cty.String,
|
2018-05-04 21:24:06 -05:00
|
|
|
},
|
2016-09-16 16:18:43 -05:00
|
|
|
}
|
|
|
|
|
2018-05-04 21:24:06 -05:00
|
|
|
{
|
|
|
|
expected := []addrs.Referenceable{
|
|
|
|
addrs.InputVariable{Name: "foo"},
|
|
|
|
}
|
|
|
|
actual := n.ReferenceableAddrs()
|
|
|
|
if !reflect.DeepEqual(actual, expected) {
|
|
|
|
t.Fatalf("%#v != %#v", actual, expected)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
gotSelfPath, gotReferencePath := n.ReferenceOutside()
|
2020-10-14 08:10:37 -05:00
|
|
|
wantSelfPath := addrs.RootModuleInstance
|
2018-05-04 21:24:06 -05:00
|
|
|
wantReferencePath := addrs.RootModuleInstance
|
|
|
|
if got, want := gotSelfPath.String(), wantSelfPath.String(); got != want {
|
|
|
|
t.Errorf("wrong self path\ngot: %s\nwant: %s", got, want)
|
|
|
|
}
|
|
|
|
if got, want := gotReferencePath.String(), wantReferencePath.String(); got != want {
|
|
|
|
t.Errorf("wrong reference path\ngot: %s\nwant: %s", got, want)
|
|
|
|
}
|
2016-09-16 16:18:43 -05:00
|
|
|
}
|
2018-05-04 21:24:06 -05:00
|
|
|
|
2016-09-16 16:18:43 -05:00
|
|
|
}
|
|
|
|
|
2020-10-14 08:10:37 -05:00
|
|
|
func TestNodeModuleVariableReference(t *testing.T) {
|
2020-04-07 16:11:32 -05:00
|
|
|
n := &nodeExpandModuleVariable{
|
2020-10-14 08:10:37 -05:00
|
|
|
Addr: addrs.InputVariable{Name: "foo"},
|
|
|
|
Module: addrs.RootModule.Child("bar"),
|
2018-05-04 21:24:06 -05:00
|
|
|
Config: &configs.Variable{
|
2021-09-10 09:58:44 -05:00
|
|
|
Name: "foo",
|
|
|
|
Type: cty.String,
|
|
|
|
ConstraintType: cty.String,
|
2018-05-04 21:24:06 -05:00
|
|
|
},
|
|
|
|
Expr: &hclsyntax.ScopeTraversalExpr{
|
|
|
|
Traversal: hcl.Traversal{
|
|
|
|
hcl.TraverseRoot{Name: "var"},
|
|
|
|
hcl.TraverseAttr{Name: "foo"},
|
|
|
|
},
|
|
|
|
},
|
2016-09-16 16:18:43 -05:00
|
|
|
}
|
|
|
|
|
2018-05-10 17:59:47 -05:00
|
|
|
want := []*addrs.Reference{
|
2018-05-04 21:24:06 -05:00
|
|
|
{
|
|
|
|
Subject: addrs.InputVariable{Name: "foo"},
|
|
|
|
},
|
|
|
|
}
|
2018-05-10 17:59:47 -05:00
|
|
|
got := n.References()
|
|
|
|
for _, problem := range deep.Equal(got, want) {
|
|
|
|
t.Error(problem)
|
2016-09-16 16:18:43 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-14 08:10:37 -05:00
|
|
|
func TestNodeModuleVariableReference_grandchild(t *testing.T) {
|
2020-04-07 16:11:32 -05:00
|
|
|
n := &nodeExpandModuleVariable{
|
2020-10-14 08:10:37 -05:00
|
|
|
Addr: addrs.InputVariable{Name: "foo"},
|
|
|
|
Module: addrs.RootModule.Child("bar"),
|
2018-05-04 21:24:06 -05:00
|
|
|
Config: &configs.Variable{
|
2021-09-10 09:58:44 -05:00
|
|
|
Name: "foo",
|
|
|
|
Type: cty.String,
|
|
|
|
ConstraintType: cty.String,
|
2018-05-04 21:24:06 -05:00
|
|
|
},
|
|
|
|
Expr: &hclsyntax.ScopeTraversalExpr{
|
|
|
|
Traversal: hcl.Traversal{
|
|
|
|
hcl.TraverseRoot{Name: "var"},
|
|
|
|
hcl.TraverseAttr{Name: "foo"},
|
|
|
|
},
|
|
|
|
},
|
2016-09-16 16:18:43 -05:00
|
|
|
}
|
|
|
|
|
2018-05-10 17:59:47 -05:00
|
|
|
want := []*addrs.Reference{
|
2018-05-04 21:24:06 -05:00
|
|
|
{
|
|
|
|
Subject: addrs.InputVariable{Name: "foo"},
|
|
|
|
},
|
|
|
|
}
|
2018-05-10 17:59:47 -05:00
|
|
|
got := n.References()
|
|
|
|
for _, problem := range deep.Equal(got, want) {
|
|
|
|
t.Error(problem)
|
2016-09-16 16:18:43 -05:00
|
|
|
}
|
|
|
|
}
|
2023-11-08 17:07:09 -06:00
|
|
|
|
|
|
|
func TestNodeModuleVariableConstraints(t *testing.T) {
|
|
|
|
// This is a little extra convoluted to poke at some edge cases that have cropped up in the past around
|
|
|
|
// evaluating dependent nodes between the plan -> apply and destroy cycle.
|
|
|
|
m := testModuleInline(t, map[string]string{
|
|
|
|
"main.tf": `
|
|
|
|
variable "input" {
|
|
|
|
type = string
|
|
|
|
validation {
|
|
|
|
condition = var.input != ""
|
|
|
|
error_message = "Input must not be empty."
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module "child" {
|
|
|
|
source = "./child"
|
|
|
|
input = var.input
|
|
|
|
}
|
|
|
|
|
|
|
|
provider "test" {
|
|
|
|
alias = "secondary"
|
|
|
|
test_string = module.child.output
|
|
|
|
}
|
|
|
|
|
|
|
|
resource "test_object" "resource" {
|
|
|
|
provider = test.secondary
|
|
|
|
test_string = "test string"
|
|
|
|
}
|
|
|
|
|
|
|
|
`,
|
|
|
|
"child/main.tf": `
|
|
|
|
variable "input" {
|
|
|
|
type = string
|
|
|
|
validation {
|
|
|
|
condition = var.input != ""
|
|
|
|
error_message = "Input must not be empty."
|
|
|
|
}
|
|
|
|
}
|
|
|
|
provider "test" {
|
|
|
|
test_string = "foo"
|
|
|
|
}
|
|
|
|
resource "test_object" "resource" {
|
|
|
|
test_string = var.input
|
|
|
|
}
|
|
|
|
output "output" {
|
|
|
|
value = test_object.resource.id
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
})
|
|
|
|
|
|
|
|
checkableObjects := []addrs.Checkable{
|
|
|
|
addrs.InputVariable{Name: "input"}.Absolute(addrs.RootModuleInstance),
|
|
|
|
addrs.InputVariable{Name: "input"}.Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey)),
|
|
|
|
}
|
|
|
|
|
|
|
|
p := &MockProvider{
|
|
|
|
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
|
|
|
|
Provider: providers.Schema{Block: simpleTestSchema()},
|
|
|
|
ResourceTypes: map[string]providers.Schema{
|
|
|
|
"test_object": providers.Schema{Block: &configschema.Block{
|
|
|
|
Attributes: map[string]*configschema.Attribute{
|
|
|
|
"id": {
|
|
|
|
Type: cty.String,
|
|
|
|
Computed: true,
|
|
|
|
},
|
|
|
|
"test_string": {
|
|
|
|
Type: cty.String,
|
|
|
|
Required: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
|
|
|
|
if req.Config.GetAttr("test_string").IsNull() {
|
|
|
|
resp.Diagnostics = resp.Diagnostics.Append(errors.New("missing test_string value"))
|
|
|
|
}
|
|
|
|
return resp
|
|
|
|
}
|
|
|
|
|
|
|
|
ctxOpts := &ContextOpts{
|
|
|
|
Providers: map[addrs.Provider]providers.Factory{
|
|
|
|
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
t.Run("pass", func(t *testing.T) {
|
|
|
|
ctx := testContext2(t, ctxOpts)
|
|
|
|
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
|
|
|
Mode: plans.NormalMode,
|
|
|
|
SetVariables: InputValues{
|
|
|
|
"input": &InputValue{
|
|
|
|
Value: cty.StringVal("beep"),
|
|
|
|
SourceType: ValueFromCLIArg,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
assertNoDiagnostics(t, diags)
|
|
|
|
|
|
|
|
for _, addr := range checkableObjects {
|
|
|
|
result := plan.Checks.GetObjectResult(addr)
|
|
|
|
if result == nil {
|
|
|
|
t.Fatalf("no check result for %s in the plan", addr)
|
|
|
|
}
|
|
|
|
if got, want := result.Status, checks.StatusPass; got != want {
|
|
|
|
t.Fatalf("wrong check status for %s during planning\ngot: %s\nwant: %s", addr, got, want)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
state, diags := ctx.Apply(plan, m)
|
|
|
|
assertNoDiagnostics(t, diags)
|
|
|
|
for _, addr := range checkableObjects {
|
|
|
|
result := state.CheckResults.GetObjectResult(addr)
|
|
|
|
if result == nil {
|
|
|
|
t.Fatalf("no check result for %s in the final state", addr)
|
|
|
|
}
|
|
|
|
if got, want := result.Status, checks.StatusPass; got != want {
|
|
|
|
t.Errorf("wrong check status for %s after apply\ngot: %s\nwant: %s", addr, got, want)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
plan, diags = ctx.Plan(m, state, &PlanOpts{
|
|
|
|
Mode: plans.DestroyMode,
|
|
|
|
SetVariables: InputValues{
|
|
|
|
"input": &InputValue{
|
|
|
|
Value: cty.StringVal("beep"),
|
|
|
|
SourceType: ValueFromCLIArg,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
assertNoDiagnostics(t, diags)
|
|
|
|
|
|
|
|
state, diags = ctx.Apply(plan, m)
|
|
|
|
assertNoDiagnostics(t, diags)
|
|
|
|
for _, addr := range checkableObjects {
|
|
|
|
result := state.CheckResults.GetObjectResult(addr)
|
|
|
|
if result == nil {
|
|
|
|
t.Fatalf("no check result for %s in the final state", addr)
|
|
|
|
}
|
|
|
|
if got, want := result.Status, checks.StatusPass; got != want {
|
|
|
|
t.Errorf("wrong check status for %s after apply\ngot: %s\nwant: %s", addr, got, want)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("fail", func(t *testing.T) {
|
|
|
|
ctx := testContext2(t, ctxOpts)
|
|
|
|
_, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
|
|
|
|
Mode: plans.NormalMode,
|
|
|
|
SetVariables: InputValues{
|
|
|
|
"input": &InputValue{
|
|
|
|
Value: cty.StringVal(""),
|
|
|
|
SourceType: ValueFromCLIArg,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
if !diags.HasErrors() {
|
|
|
|
t.Fatalf("succeeded; want error")
|
|
|
|
}
|
|
|
|
|
|
|
|
const wantSummary = "Invalid value for variable"
|
|
|
|
found := false
|
|
|
|
for _, diag := range diags {
|
|
|
|
if diag.Severity() == tfdiags.Error && diag.Description().Summary == wantSummary {
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !found {
|
|
|
|
t.Fatalf("missing expected error\nwant summary: %s\ngot: %s", wantSummary, diags.Err().Error())
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|