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, Plugins: schemaOnlyProvidersForTesting(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, Plugins: schemaOnlyProvidersForTesting(schemas.Providers), } 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, } }