mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
In order to generate OpenTelemetry traces of the main work that OpenTofu Core does we'll need to be able to propagate the active trace context into the main "Execute" method of each graph node, since that's where we typically make requests to providers, and other such work that could take a noticeable amount of time. Changing these frequently-used interfaces is always a noisy diff, so this commit intentionally focuses only on changing the signature of that interface and its one caller, and then dealing with all of the fallout of that on existing unit test code. For any use of Execute that was affected by this change we'll also switch to our newer naming scheme of using "evalCtx" as the name of the tofu.EvalContext variable, in preparation for using "ctx" idiomatically to refer to context.Context. However, the implementations currently don't yet name their context.Context argument because the method bodies don't yet make use of it. We'll name each of those arguments to "ctx" individually as we gradually add tracing support to each graph node type. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
216 lines
6.8 KiB
Go
216 lines
6.8 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 tofu
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hcltest"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/checks"
|
|
"github.com/opentofu/opentofu/internal/configs"
|
|
"github.com/opentofu/opentofu/internal/lang"
|
|
)
|
|
|
|
func TestNodeRootVariableExecute(t *testing.T) {
|
|
t.Run("type conversion", func(t *testing.T) {
|
|
evalCtx := new(MockEvalContext)
|
|
|
|
n := &NodeRootVariable{
|
|
Addr: addrs.InputVariable{Name: "foo"},
|
|
Config: &configs.Variable{
|
|
Name: "foo",
|
|
Type: cty.String,
|
|
ConstraintType: cty.String,
|
|
},
|
|
RawValue: &InputValue{
|
|
Value: cty.True,
|
|
SourceType: ValueFromUnknown,
|
|
},
|
|
}
|
|
|
|
evalCtx.ChecksState = checks.NewState(&configs.Config{
|
|
Module: &configs.Module{
|
|
Variables: map[string]*configs.Variable{
|
|
"foo": n.Config,
|
|
},
|
|
},
|
|
})
|
|
|
|
diags := n.Execute(t.Context(), evalCtx, walkApply)
|
|
if diags.HasErrors() {
|
|
t.Fatalf("unexpected error: %s", diags.Err())
|
|
}
|
|
|
|
if !evalCtx.SetRootModuleArgumentCalled {
|
|
t.Fatalf("ctx.SetRootModuleArgument wasn't called")
|
|
}
|
|
if got, want := evalCtx.SetRootModuleArgumentAddr.String(), "var.foo"; got != want {
|
|
t.Errorf("wrong address for ctx.SetRootModuleArgument\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
if got, want := evalCtx.SetRootModuleArgumentValue, cty.StringVal("true"); !want.RawEquals(got) {
|
|
// NOTE: The given value was cty.Bool but the type constraint was
|
|
// cty.String, so it was NodeRootVariable's responsibility to convert
|
|
// as part of preparing the "final value".
|
|
t.Errorf("wrong value for ctx.SetRootModuleArgument\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
})
|
|
t.Run("validation", func(t *testing.T) {
|
|
evalCtx := new(MockEvalContext)
|
|
|
|
// The variable validation function gets called with OpenTofu's
|
|
// built-in functions available, so we need a minimal scope just for
|
|
// it to get the functions from.
|
|
evalCtx.EvaluationScopeScope = &lang.Scope{}
|
|
|
|
// We need to reimplement a _little_ bit of EvalContextBuiltin logic
|
|
// here to get a similar effect with EvalContextMock just to get the
|
|
// value to flow through here in a realistic way that'll make this test
|
|
// useful.
|
|
var finalVal cty.Value
|
|
evalCtx.SetRootModuleArgumentFunc = func(addr addrs.InputVariable, v cty.Value) {
|
|
if addr.Name == "foo" {
|
|
t.Logf("set %s to %#v", addr.String(), v)
|
|
finalVal = v
|
|
}
|
|
}
|
|
evalCtx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value {
|
|
if addr.String() != "var.foo" {
|
|
return cty.NilVal
|
|
}
|
|
t.Logf("reading final val for %s (%#v)", addr.String(), finalVal)
|
|
return finalVal
|
|
}
|
|
|
|
n := &NodeRootVariable{
|
|
Addr: addrs.InputVariable{Name: "foo"},
|
|
Config: &configs.Variable{
|
|
Name: "foo",
|
|
Type: cty.Number,
|
|
ConstraintType: cty.Number,
|
|
Validations: []*configs.CheckRule{
|
|
{
|
|
Condition: fakeHCLExpressionFunc(func(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
// This returns true only if the given variable value
|
|
// is exactly cty.Number, which allows us to verify
|
|
// that we were given the value _after_ type
|
|
// conversion.
|
|
// This had previously not been handled correctly,
|
|
// as reported in:
|
|
// https://github.com/hashicorp/terraform/issues/29899
|
|
vars := ctx.Variables["var"]
|
|
if vars == cty.NilVal || !vars.Type().IsObjectType() || !vars.Type().HasAttribute("foo") {
|
|
t.Logf("var.foo isn't available")
|
|
return cty.False, nil
|
|
}
|
|
val := vars.GetAttr("foo")
|
|
if val == cty.NilVal || val.Type() != cty.Number {
|
|
t.Logf("var.foo is %#v; want a number", val)
|
|
return cty.False, nil
|
|
}
|
|
return cty.True, nil
|
|
}),
|
|
ErrorMessage: hcltest.MockExprLiteral(cty.StringVal("Must be a number.")),
|
|
},
|
|
},
|
|
},
|
|
RawValue: &InputValue{
|
|
// Note: This is a string, but the variable's type constraint
|
|
// is number so it should be converted before use.
|
|
Value: cty.StringVal("5"),
|
|
SourceType: ValueFromUnknown,
|
|
},
|
|
}
|
|
|
|
ref := &nodeVariableReference{
|
|
Addr: n.Addr,
|
|
Config: n.Config,
|
|
}
|
|
|
|
evalCtx.ChecksState = checks.NewState(&configs.Config{
|
|
Module: &configs.Module{
|
|
Variables: map[string]*configs.Variable{
|
|
"foo": n.Config,
|
|
},
|
|
},
|
|
})
|
|
|
|
diags := n.Execute(t.Context(), evalCtx, walkApply)
|
|
if diags.HasErrors() {
|
|
t.Fatalf("unexpected error: %s", diags.Err())
|
|
}
|
|
|
|
g, err := ref.DynamicExpand(evalCtx)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
for _, v := range g.Vertices() {
|
|
if ev, ok := v.(GraphNodeExecutable); ok {
|
|
diags = ev.Execute(t.Context(), evalCtx, walkApply)
|
|
if diags.HasErrors() {
|
|
t.Fatalf("unexpected error: %s", diags.Err())
|
|
}
|
|
}
|
|
}
|
|
|
|
if !evalCtx.SetRootModuleArgumentCalled {
|
|
t.Fatalf("ctx.SetRootModuleArgument wasn't called")
|
|
}
|
|
if got, want := evalCtx.SetRootModuleArgumentAddr.String(), "var.foo"; got != want {
|
|
t.Errorf("wrong address for ctx.SetRootModuleArgument\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
if got, want := evalCtx.SetRootModuleArgumentValue, cty.NumberIntVal(5); !want.RawEquals(got) {
|
|
// NOTE: The given value was cty.Bool but the type constraint was
|
|
// cty.String, so it was NodeRootVariable's responsibility to convert
|
|
// as part of preparing the "final value".
|
|
t.Errorf("wrong value for ctx.SetRootModuleArgument\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
if status := evalCtx.Checks().ObjectCheckStatus(n.Addr.Absolute(addrs.RootModuleInstance)); status != checks.StatusPass {
|
|
t.Errorf("expected checks to pass but go %s instead", status)
|
|
}
|
|
})
|
|
}
|
|
|
|
// fakeHCLExpressionFunc is a fake implementation of hcl.Expression that just
|
|
// directly produces a value with direct Go code.
|
|
//
|
|
// An expression of this type has no references and so it cannot access any
|
|
// variables from the EvalContext unless something else arranges for them
|
|
// to be guaranteed available. For example, custom variable validations just
|
|
// unconditionally have access to the variable they are validating regardless
|
|
// of references.
|
|
type fakeHCLExpressionFunc func(*hcl.EvalContext) (cty.Value, hcl.Diagnostics)
|
|
|
|
var _ hcl.Expression = fakeHCLExpressionFunc(nil)
|
|
|
|
func (f fakeHCLExpressionFunc) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
return f(ctx)
|
|
}
|
|
|
|
func (f fakeHCLExpressionFunc) Variables() []hcl.Traversal {
|
|
return nil
|
|
}
|
|
|
|
func (f fakeHCLExpressionFunc) Functions() []hcl.Traversal {
|
|
return nil
|
|
}
|
|
|
|
func (f fakeHCLExpressionFunc) Range() hcl.Range {
|
|
return hcl.Range{
|
|
Filename: "fake",
|
|
Start: hcl.InitialPos,
|
|
End: hcl.InitialPos,
|
|
}
|
|
}
|
|
|
|
func (f fakeHCLExpressionFunc) StartRange() hcl.Range {
|
|
return f.Range()
|
|
}
|