opentofu/internal/terraform/evaluate_test.go
Martin Atkins e5f52e56f8 addrs: Rename DefaultRegistryHost to DefaultProviderRegistryHost
As the comment notes, this hostname is the default for provide source
addresses. We'll shortly be adding some address types to represent module
source addresses too, and so we'll also have DefaultModuleRegistryHost
for that situation.

(They'll actually both contain the the same hostname, but that's a
coincidence rather than a requirement.)
2021-06-03 08:50:34 -07:00

564 lines
15 KiB
Go

package terraform
import (
"sync"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
)
func TestEvaluatorGetTerraformAttr(t *testing.T) {
evaluator := &Evaluator{
Meta: &ContextMeta{
Env: "foo",
},
}
data := &evaluationStateData{
Evaluator: evaluator,
}
scope := evaluator.Scope(data, nil)
t.Run("workspace", func(t *testing.T) {
want := cty.StringVal("foo")
got, diags := scope.Data.GetTerraformAttr(addrs.TerraformAttr{
Name: "workspace",
}, tfdiags.SourceRange{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result %q; want %q", got, want)
}
})
}
func TestEvaluatorGetPathAttr(t *testing.T) {
evaluator := &Evaluator{
Meta: &ContextMeta{
Env: "foo",
},
Config: &configs.Config{
Module: &configs.Module{
SourceDir: "bar/baz",
},
},
}
data := &evaluationStateData{
Evaluator: evaluator,
}
scope := evaluator.Scope(data, nil)
t.Run("module", func(t *testing.T) {
want := cty.StringVal("bar/baz")
got, diags := scope.Data.GetPathAttr(addrs.PathAttr{
Name: "module",
}, tfdiags.SourceRange{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result %#v; want %#v", got, want)
}
})
t.Run("root", func(t *testing.T) {
want := cty.StringVal("bar/baz")
got, diags := scope.Data.GetPathAttr(addrs.PathAttr{
Name: "root",
}, tfdiags.SourceRange{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result %#v; want %#v", got, want)
}
})
}
// This particularly tests that a sensitive attribute in config
// results in a value that has a "sensitive" cty Mark
func TestEvaluatorGetInputVariable(t *testing.T) {
evaluator := &Evaluator{
Meta: &ContextMeta{
Env: "foo",
},
Config: &configs.Config{
Module: &configs.Module{
Variables: map[string]*configs.Variable{
"some_var": {
Name: "some_var",
Sensitive: true,
Default: cty.StringVal("foo"),
},
// Avoid double marking a value
"some_other_var": {
Name: "some_other_var",
Sensitive: true,
Default: cty.StringVal("bar"),
},
},
},
},
VariableValues: map[string]map[string]cty.Value{
"": {
"some_var": cty.StringVal("bar"),
"some_other_var": cty.StringVal("boop").Mark("sensitive"),
},
},
VariableValuesLock: &sync.Mutex{},
}
data := &evaluationStateData{
Evaluator: evaluator,
}
scope := evaluator.Scope(data, nil)
want := cty.StringVal("bar").Mark("sensitive")
got, diags := scope.Data.GetInputVariable(addrs.InputVariable{
Name: "some_var",
}, tfdiags.SourceRange{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result %#v; want %#v", got, want)
}
want = cty.StringVal("boop").Mark("sensitive")
got, diags = scope.Data.GetInputVariable(addrs.InputVariable{
Name: "some_other_var",
}, tfdiags.SourceRange{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result %#v; want %#v", got, want)
}
}
func TestEvaluatorGetResource(t *testing.T) {
stateSync := states.BuildState(func(ss *states.SyncState) {
ss.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo", "nesting_list": [{"sensitive_value":"abc"}], "nesting_map": {"foo":{"foo":"x"}}, "nesting_set": [{"baz":"abc"}], "nesting_single": {"boop":"abc"}, "nesting_nesting": {"nesting_list":[{"sensitive_value":"abc"}]}, "value":"hello"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
}).SyncWrapper()
rc := &configs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "foo",
Config: configs.SynthBody("", map[string]cty.Value{
"id": cty.StringVal("foo"),
}),
Provider: addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "hashicorp",
Type: "test",
},
}
evaluator := &Evaluator{
Meta: &ContextMeta{
Env: "foo",
},
Changes: plans.NewChanges().SyncWrapper(),
Config: &configs.Config{
Module: &configs.Module{
ManagedResources: map[string]*configs.Resource{
"test_resource.foo": rc,
},
},
},
State: stateSync,
Schemas: &Schemas{
Providers: map[addrs.Provider]*ProviderSchema{
addrs.NewDefaultProvider("test"): {
Provider: &configschema.Block{},
ResourceTypes: map[string]*configschema.Block{
"test_resource": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"value": {
Type: cty.String,
Computed: true,
Sensitive: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"nesting_list": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {Type: cty.String, Optional: true},
"sensitive_value": {Type: cty.String, Optional: true, Sensitive: true},
},
},
Nesting: configschema.NestingList,
},
"nesting_map": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {Type: cty.String, Optional: true, Sensitive: true},
},
},
Nesting: configschema.NestingMap,
},
"nesting_set": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"baz": {Type: cty.String, Optional: true, Sensitive: true},
},
},
Nesting: configschema.NestingSet,
},
"nesting_single": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"boop": {Type: cty.String, Optional: true, Sensitive: true},
},
},
Nesting: configschema.NestingSingle,
},
"nesting_nesting": {
Block: configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"nesting_list": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {Type: cty.String, Optional: true},
"sensitive_value": {Type: cty.String, Optional: true, Sensitive: true},
},
},
Nesting: configschema.NestingList,
},
},
},
Nesting: configschema.NestingSingle,
},
},
},
},
},
},
},
}
data := &evaluationStateData{
Evaluator: evaluator,
}
scope := evaluator.Scope(data, nil)
want := cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo"),
"nesting_list": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"sensitive_value": cty.StringVal("abc").Mark("sensitive"),
"value": cty.NullVal(cty.String),
}),
}),
"nesting_map": cty.MapVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("x").Mark("sensitive")}),
}),
"nesting_nesting": cty.ObjectVal(map[string]cty.Value{
"nesting_list": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"sensitive_value": cty.StringVal("abc").Mark("sensitive"),
"value": cty.NullVal(cty.String),
}),
}),
}),
"nesting_set": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("abc").Mark("sensitive"),
}),
}),
"nesting_single": cty.ObjectVal(map[string]cty.Value{
"boop": cty.StringVal("abc").Mark("sensitive"),
}),
"value": cty.StringVal("hello").Mark("sensitive"),
})
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "foo",
}
got, diags := scope.Data.GetResource(addr, tfdiags.SourceRange{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result:\ngot: %#v\nwant: %#v", got, want)
}
}
// GetResource will return a planned object's After value
// if there is a change for that resource instance.
func TestEvaluatorGetResource_changes(t *testing.T) {
// Set up existing state
stateSync := states.BuildState(func(ss *states.SyncState) {
ss.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectPlanned,
AttrsJSON: []byte(`{"id":"foo", "to_mark_val":"tacos", "sensitive_value":"abc"}`),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
}).SyncWrapper()
// Create a change for the existing state resource,
// to exercise retrieving the After value of the change
changesSync := plans.NewChanges().SyncWrapper()
change := &plans.ResourceInstanceChange{
Addr: mustResourceInstanceAddr("test_resource.foo"),
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
},
Change: plans.Change{
Action: plans.Update,
// Provide an After value that contains a marked value
After: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo"),
"to_mark_val": cty.StringVal("pizza").Mark("sensitive"),
"sensitive_value": cty.StringVal("abc"),
"sensitive_collection": cty.MapVal(map[string]cty.Value{
"boop": cty.StringVal("beep"),
}),
}),
},
}
// Set up our schemas
schemas := &Schemas{
Providers: map[addrs.Provider]*ProviderSchema{
addrs.NewDefaultProvider("test"): {
Provider: &configschema.Block{},
ResourceTypes: map[string]*configschema.Block{
"test_resource": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"to_mark_val": {
Type: cty.String,
Computed: true,
},
"sensitive_value": {
Type: cty.String,
Computed: true,
Sensitive: true,
},
"sensitive_collection": {
Type: cty.Map(cty.String),
Computed: true,
Sensitive: true,
},
},
},
},
},
},
}
// The resource we'll inspect
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "foo",
}
schema, _ := schemas.ResourceTypeConfig(addrs.NewDefaultProvider("test"), addr.Mode, addr.Type)
// This encoding separates out the After's marks into its AfterValMarks
csrc, _ := change.Encode(schema.ImpliedType())
changesSync.AppendResourceInstanceChange(csrc)
evaluator := &Evaluator{
Meta: &ContextMeta{
Env: "foo",
},
Changes: changesSync,
Config: &configs.Config{
Module: &configs.Module{
ManagedResources: map[string]*configs.Resource{
"test_resource.foo": {
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "foo",
Provider: addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "hashicorp",
Type: "test",
},
},
},
},
},
State: stateSync,
Schemas: schemas,
}
data := &evaluationStateData{
Evaluator: evaluator,
}
scope := evaluator.Scope(data, nil)
want := cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("foo"),
"to_mark_val": cty.StringVal("pizza").Mark("sensitive"),
"sensitive_value": cty.StringVal("abc").Mark("sensitive"),
"sensitive_collection": cty.MapVal(map[string]cty.Value{
"boop": cty.StringVal("beep"),
}).Mark("sensitive"),
})
got, diags := scope.Data.GetResource(addr, tfdiags.SourceRange{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result:\ngot: %#v\nwant: %#v", got, want)
}
}
func TestEvaluatorGetModule(t *testing.T) {
// Create a new evaluator with an existing state
stateSync := states.BuildState(func(ss *states.SyncState) {
ss.SetOutputValue(
addrs.OutputValue{Name: "out"}.Absolute(addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "mod"}}),
cty.StringVal("bar"),
true,
)
}).SyncWrapper()
evaluator := evaluatorForModule(stateSync, plans.NewChanges().SyncWrapper())
data := &evaluationStateData{
Evaluator: evaluator,
}
scope := evaluator.Scope(data, nil)
want := cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("bar").Mark("sensitive")})
got, diags := scope.Data.GetModule(addrs.ModuleCall{
Name: "mod",
}, tfdiags.SourceRange{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result %#v; want %#v", got, want)
}
// Changes should override the state value
changesSync := plans.NewChanges().SyncWrapper()
change := &plans.OutputChange{
Addr: addrs.OutputValue{Name: "out"}.Absolute(addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "mod"}}),
Sensitive: true,
Change: plans.Change{
After: cty.StringVal("baz"),
},
}
cs, _ := change.Encode()
changesSync.AppendOutputChange(cs)
evaluator = evaluatorForModule(stateSync, changesSync)
data = &evaluationStateData{
Evaluator: evaluator,
}
scope = evaluator.Scope(data, nil)
want = cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("baz").Mark("sensitive")})
got, diags = scope.Data.GetModule(addrs.ModuleCall{
Name: "mod",
}, tfdiags.SourceRange{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result %#v; want %#v", got, want)
}
// Test changes with empty state
evaluator = evaluatorForModule(states.NewState().SyncWrapper(), changesSync)
data = &evaluationStateData{
Evaluator: evaluator,
}
scope = evaluator.Scope(data, nil)
want = cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("baz").Mark("sensitive")})
got, diags = scope.Data.GetModule(addrs.ModuleCall{
Name: "mod",
}, tfdiags.SourceRange{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result %#v; want %#v", got, want)
}
}
func evaluatorForModule(stateSync *states.SyncState, changesSync *plans.ChangesSync) *Evaluator {
return &Evaluator{
Meta: &ContextMeta{
Env: "foo",
},
Config: &configs.Config{
Module: &configs.Module{
ModuleCalls: map[string]*configs.ModuleCall{
"mod": {
Name: "mod",
},
},
},
Children: map[string]*configs.Config{
"mod": {
Path: addrs.Module{"module.mod"},
Module: &configs.Module{
Outputs: map[string]*configs.Output{
"out": {
Name: "out",
Sensitive: true,
},
},
},
},
},
},
State: stateSync,
Changes: changesSync,
}
}