| 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, |
| } |
| } |