mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-03 20:57:09 -06:00
53a73a8ab6
In order to handle optional attributes, the Variable type needs to keep track of the type constraint for decoding and conversion, as well as the concrete type for creating values and type comparison. Since the Type field is referenced throughout the codebase, and for future refactoring if the handling of optional attributes changes significantly, the constraint is now loaded into an entirely new field called ConstraintType. This prevents types containing ObjectWithOptionalAttrs from escaping the decode/conversion codepaths into the rest of the codebase.
569 lines
16 KiB
Go
569 lines
16 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/lang/marks"
|
|
"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"),
|
|
Type: cty.String,
|
|
ConstraintType: cty.String,
|
|
},
|
|
// Avoid double marking a value
|
|
"some_other_var": {
|
|
Name: "some_other_var",
|
|
Sensitive: true,
|
|
Default: cty.StringVal("bar"),
|
|
Type: cty.String,
|
|
ConstraintType: cty.String,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
VariableValues: map[string]map[string]cty.Value{
|
|
"": {
|
|
"some_var": cty.StringVal("bar"),
|
|
"some_other_var": cty.StringVal("boop").Mark(marks.Sensitive),
|
|
},
|
|
},
|
|
VariableValuesLock: &sync.Mutex{},
|
|
}
|
|
|
|
data := &evaluationStateData{
|
|
Evaluator: evaluator,
|
|
}
|
|
scope := evaluator.Scope(data, nil)
|
|
|
|
want := cty.StringVal("bar").Mark(marks.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(marks.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(marks.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(marks.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(marks.Sensitive),
|
|
"value": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
}),
|
|
"nesting_set": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"baz": cty.StringVal("abc").Mark(marks.Sensitive),
|
|
}),
|
|
}),
|
|
"nesting_single": cty.ObjectVal(map[string]cty.Value{
|
|
"boop": cty.StringVal("abc").Mark(marks.Sensitive),
|
|
}),
|
|
"value": cty.StringVal("hello").Mark(marks.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(marks.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(marks.Sensitive),
|
|
"sensitive_value": cty.StringVal("abc").Mark(marks.Sensitive),
|
|
"sensitive_collection": cty.MapVal(map[string]cty.Value{
|
|
"boop": cty.StringVal("beep"),
|
|
}).Mark(marks.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(marks.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(marks.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(marks.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,
|
|
}
|
|
}
|