github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/terraform/evaluate_test.go (about)

     1  package terraform
     2  
     3  import (
     4  	"sync"
     5  	"testing"
     6  
     7  	"github.com/davecgh/go-spew/spew"
     8  	"github.com/zclconf/go-cty/cty"
     9  
    10  	"github.com/hashicorp/terraform/internal/addrs"
    11  	"github.com/hashicorp/terraform/internal/configs"
    12  	"github.com/hashicorp/terraform/internal/configs/configschema"
    13  	"github.com/hashicorp/terraform/internal/lang/marks"
    14  	"github.com/hashicorp/terraform/internal/plans"
    15  	"github.com/hashicorp/terraform/internal/states"
    16  	"github.com/hashicorp/terraform/internal/tfdiags"
    17  )
    18  
    19  func TestEvaluatorGetTerraformAttr(t *testing.T) {
    20  	evaluator := &Evaluator{
    21  		Meta: &ContextMeta{
    22  			Env: "foo",
    23  		},
    24  	}
    25  	data := &evaluationStateData{
    26  		Evaluator: evaluator,
    27  	}
    28  	scope := evaluator.Scope(data, nil)
    29  
    30  	t.Run("workspace", func(t *testing.T) {
    31  		want := cty.StringVal("foo")
    32  		got, diags := scope.Data.GetTerraformAttr(addrs.TerraformAttr{
    33  			Name: "workspace",
    34  		}, tfdiags.SourceRange{})
    35  		if len(diags) != 0 {
    36  			t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
    37  		}
    38  		if !got.RawEquals(want) {
    39  			t.Errorf("wrong result %q; want %q", got, want)
    40  		}
    41  	})
    42  }
    43  
    44  func TestEvaluatorGetPathAttr(t *testing.T) {
    45  	evaluator := &Evaluator{
    46  		Meta: &ContextMeta{
    47  			Env: "foo",
    48  		},
    49  		Config: &configs.Config{
    50  			Module: &configs.Module{
    51  				SourceDir: "bar/baz",
    52  			},
    53  		},
    54  	}
    55  	data := &evaluationStateData{
    56  		Evaluator: evaluator,
    57  	}
    58  	scope := evaluator.Scope(data, nil)
    59  
    60  	t.Run("module", func(t *testing.T) {
    61  		want := cty.StringVal("bar/baz")
    62  		got, diags := scope.Data.GetPathAttr(addrs.PathAttr{
    63  			Name: "module",
    64  		}, tfdiags.SourceRange{})
    65  		if len(diags) != 0 {
    66  			t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
    67  		}
    68  		if !got.RawEquals(want) {
    69  			t.Errorf("wrong result %#v; want %#v", got, want)
    70  		}
    71  	})
    72  
    73  	t.Run("root", func(t *testing.T) {
    74  		want := cty.StringVal("bar/baz")
    75  		got, diags := scope.Data.GetPathAttr(addrs.PathAttr{
    76  			Name: "root",
    77  		}, tfdiags.SourceRange{})
    78  		if len(diags) != 0 {
    79  			t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
    80  		}
    81  		if !got.RawEquals(want) {
    82  			t.Errorf("wrong result %#v; want %#v", got, want)
    83  		}
    84  	})
    85  }
    86  
    87  // This particularly tests that a sensitive attribute in config
    88  // results in a value that has a "sensitive" cty Mark
    89  func TestEvaluatorGetInputVariable(t *testing.T) {
    90  	evaluator := &Evaluator{
    91  		Meta: &ContextMeta{
    92  			Env: "foo",
    93  		},
    94  		Config: &configs.Config{
    95  			Module: &configs.Module{
    96  				Variables: map[string]*configs.Variable{
    97  					"some_var": {
    98  						Name:      "some_var",
    99  						Sensitive: true,
   100  						Default:   cty.StringVal("foo"),
   101  					},
   102  					// Avoid double marking a value
   103  					"some_other_var": {
   104  						Name:      "some_other_var",
   105  						Sensitive: true,
   106  						Default:   cty.StringVal("bar"),
   107  					},
   108  				},
   109  			},
   110  		},
   111  		VariableValues: map[string]map[string]cty.Value{
   112  			"": {
   113  				"some_var":       cty.StringVal("bar"),
   114  				"some_other_var": cty.StringVal("boop").Mark(marks.Sensitive),
   115  			},
   116  		},
   117  		VariableValuesLock: &sync.Mutex{},
   118  	}
   119  
   120  	data := &evaluationStateData{
   121  		Evaluator: evaluator,
   122  	}
   123  	scope := evaluator.Scope(data, nil)
   124  
   125  	want := cty.StringVal("bar").Mark(marks.Sensitive)
   126  	got, diags := scope.Data.GetInputVariable(addrs.InputVariable{
   127  		Name: "some_var",
   128  	}, tfdiags.SourceRange{})
   129  
   130  	if len(diags) != 0 {
   131  		t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
   132  	}
   133  	if !got.RawEquals(want) {
   134  		t.Errorf("wrong result %#v; want %#v", got, want)
   135  	}
   136  
   137  	want = cty.StringVal("boop").Mark(marks.Sensitive)
   138  	got, diags = scope.Data.GetInputVariable(addrs.InputVariable{
   139  		Name: "some_other_var",
   140  	}, tfdiags.SourceRange{})
   141  
   142  	if len(diags) != 0 {
   143  		t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
   144  	}
   145  	if !got.RawEquals(want) {
   146  		t.Errorf("wrong result %#v; want %#v", got, want)
   147  	}
   148  }
   149  
   150  func TestEvaluatorGetResource(t *testing.T) {
   151  	stateSync := states.BuildState(func(ss *states.SyncState) {
   152  		ss.SetResourceInstanceCurrent(
   153  			addrs.Resource{
   154  				Mode: addrs.ManagedResourceMode,
   155  				Type: "test_resource",
   156  				Name: "foo",
   157  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   158  			&states.ResourceInstanceObjectSrc{
   159  				Status:    states.ObjectReady,
   160  				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"}`),
   161  			},
   162  			addrs.AbsProviderConfig{
   163  				Provider: addrs.NewDefaultProvider("test"),
   164  				Module:   addrs.RootModule,
   165  			},
   166  		)
   167  	}).SyncWrapper()
   168  
   169  	rc := &configs.Resource{
   170  		Mode: addrs.ManagedResourceMode,
   171  		Type: "test_resource",
   172  		Name: "foo",
   173  		Config: configs.SynthBody("", map[string]cty.Value{
   174  			"id": cty.StringVal("foo"),
   175  		}),
   176  		Provider: addrs.Provider{
   177  			Hostname:  addrs.DefaultProviderRegistryHost,
   178  			Namespace: "hashicorp",
   179  			Type:      "test",
   180  		},
   181  	}
   182  
   183  	evaluator := &Evaluator{
   184  		Meta: &ContextMeta{
   185  			Env: "foo",
   186  		},
   187  		Changes: plans.NewChanges().SyncWrapper(),
   188  		Config: &configs.Config{
   189  			Module: &configs.Module{
   190  				ManagedResources: map[string]*configs.Resource{
   191  					"test_resource.foo": rc,
   192  				},
   193  			},
   194  		},
   195  		State: stateSync,
   196  		Plugins: schemaOnlyProvidersForTesting(map[addrs.Provider]*ProviderSchema{
   197  			addrs.NewDefaultProvider("test"): {
   198  				Provider: &configschema.Block{},
   199  				ResourceTypes: map[string]*configschema.Block{
   200  					"test_resource": {
   201  						Attributes: map[string]*configschema.Attribute{
   202  							"id": {
   203  								Type:     cty.String,
   204  								Computed: true,
   205  							},
   206  							"value": {
   207  								Type:      cty.String,
   208  								Computed:  true,
   209  								Sensitive: true,
   210  							},
   211  						},
   212  						BlockTypes: map[string]*configschema.NestedBlock{
   213  							"nesting_list": {
   214  								Block: configschema.Block{
   215  									Attributes: map[string]*configschema.Attribute{
   216  										"value":           {Type: cty.String, Optional: true},
   217  										"sensitive_value": {Type: cty.String, Optional: true, Sensitive: true},
   218  									},
   219  								},
   220  								Nesting: configschema.NestingList,
   221  							},
   222  							"nesting_map": {
   223  								Block: configschema.Block{
   224  									Attributes: map[string]*configschema.Attribute{
   225  										"foo": {Type: cty.String, Optional: true, Sensitive: true},
   226  									},
   227  								},
   228  								Nesting: configschema.NestingMap,
   229  							},
   230  							"nesting_set": {
   231  								Block: configschema.Block{
   232  									Attributes: map[string]*configschema.Attribute{
   233  										"baz": {Type: cty.String, Optional: true, Sensitive: true},
   234  									},
   235  								},
   236  								Nesting: configschema.NestingSet,
   237  							},
   238  							"nesting_single": {
   239  								Block: configschema.Block{
   240  									Attributes: map[string]*configschema.Attribute{
   241  										"boop": {Type: cty.String, Optional: true, Sensitive: true},
   242  									},
   243  								},
   244  								Nesting: configschema.NestingSingle,
   245  							},
   246  							"nesting_nesting": {
   247  								Block: configschema.Block{
   248  									BlockTypes: map[string]*configschema.NestedBlock{
   249  										"nesting_list": {
   250  											Block: configschema.Block{
   251  												Attributes: map[string]*configschema.Attribute{
   252  													"value":           {Type: cty.String, Optional: true},
   253  													"sensitive_value": {Type: cty.String, Optional: true, Sensitive: true},
   254  												},
   255  											},
   256  											Nesting: configschema.NestingList,
   257  										},
   258  									},
   259  								},
   260  								Nesting: configschema.NestingSingle,
   261  							},
   262  						},
   263  					},
   264  				},
   265  			},
   266  		}),
   267  	}
   268  
   269  	data := &evaluationStateData{
   270  		Evaluator: evaluator,
   271  	}
   272  	scope := evaluator.Scope(data, nil)
   273  
   274  	want := cty.ObjectVal(map[string]cty.Value{
   275  		"id": cty.StringVal("foo"),
   276  		"nesting_list": cty.ListVal([]cty.Value{
   277  			cty.ObjectVal(map[string]cty.Value{
   278  				"sensitive_value": cty.StringVal("abc").Mark(marks.Sensitive),
   279  				"value":           cty.NullVal(cty.String),
   280  			}),
   281  		}),
   282  		"nesting_map": cty.MapVal(map[string]cty.Value{
   283  			"foo": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("x").Mark(marks.Sensitive)}),
   284  		}),
   285  		"nesting_nesting": cty.ObjectVal(map[string]cty.Value{
   286  			"nesting_list": cty.ListVal([]cty.Value{
   287  				cty.ObjectVal(map[string]cty.Value{
   288  					"sensitive_value": cty.StringVal("abc").Mark(marks.Sensitive),
   289  					"value":           cty.NullVal(cty.String),
   290  				}),
   291  			}),
   292  		}),
   293  		"nesting_set": cty.SetVal([]cty.Value{
   294  			cty.ObjectVal(map[string]cty.Value{
   295  				"baz": cty.StringVal("abc").Mark(marks.Sensitive),
   296  			}),
   297  		}),
   298  		"nesting_single": cty.ObjectVal(map[string]cty.Value{
   299  			"boop": cty.StringVal("abc").Mark(marks.Sensitive),
   300  		}),
   301  		"value": cty.StringVal("hello").Mark(marks.Sensitive),
   302  	})
   303  
   304  	addr := addrs.Resource{
   305  		Mode: addrs.ManagedResourceMode,
   306  		Type: "test_resource",
   307  		Name: "foo",
   308  	}
   309  	got, diags := scope.Data.GetResource(addr, tfdiags.SourceRange{})
   310  
   311  	if len(diags) != 0 {
   312  		t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
   313  	}
   314  
   315  	if !got.RawEquals(want) {
   316  		t.Errorf("wrong result:\ngot: %#v\nwant: %#v", got, want)
   317  	}
   318  }
   319  
   320  // GetResource will return a planned object's After value
   321  // if there is a change for that resource instance.
   322  func TestEvaluatorGetResource_changes(t *testing.T) {
   323  	// Set up existing state
   324  	stateSync := states.BuildState(func(ss *states.SyncState) {
   325  		ss.SetResourceInstanceCurrent(
   326  			addrs.Resource{
   327  				Mode: addrs.ManagedResourceMode,
   328  				Type: "test_resource",
   329  				Name: "foo",
   330  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   331  			&states.ResourceInstanceObjectSrc{
   332  				Status:    states.ObjectPlanned,
   333  				AttrsJSON: []byte(`{"id":"foo", "to_mark_val":"tacos", "sensitive_value":"abc"}`),
   334  			},
   335  			addrs.AbsProviderConfig{
   336  				Provider: addrs.NewDefaultProvider("test"),
   337  				Module:   addrs.RootModule,
   338  			},
   339  		)
   340  	}).SyncWrapper()
   341  
   342  	// Create a change for the existing state resource,
   343  	// to exercise retrieving the After value of the change
   344  	changesSync := plans.NewChanges().SyncWrapper()
   345  	change := &plans.ResourceInstanceChange{
   346  		Addr: mustResourceInstanceAddr("test_resource.foo"),
   347  		ProviderAddr: addrs.AbsProviderConfig{
   348  			Module:   addrs.RootModule,
   349  			Provider: addrs.NewDefaultProvider("test"),
   350  		},
   351  		Change: plans.Change{
   352  			Action: plans.Update,
   353  			// Provide an After value that contains a marked value
   354  			After: cty.ObjectVal(map[string]cty.Value{
   355  				"id":              cty.StringVal("foo"),
   356  				"to_mark_val":     cty.StringVal("pizza").Mark(marks.Sensitive),
   357  				"sensitive_value": cty.StringVal("abc"),
   358  				"sensitive_collection": cty.MapVal(map[string]cty.Value{
   359  					"boop": cty.StringVal("beep"),
   360  				}),
   361  			}),
   362  		},
   363  	}
   364  
   365  	// Set up our schemas
   366  	schemas := &Schemas{
   367  		Providers: map[addrs.Provider]*ProviderSchema{
   368  			addrs.NewDefaultProvider("test"): {
   369  				Provider: &configschema.Block{},
   370  				ResourceTypes: map[string]*configschema.Block{
   371  					"test_resource": {
   372  						Attributes: map[string]*configschema.Attribute{
   373  							"id": {
   374  								Type:     cty.String,
   375  								Computed: true,
   376  							},
   377  							"to_mark_val": {
   378  								Type:     cty.String,
   379  								Computed: true,
   380  							},
   381  							"sensitive_value": {
   382  								Type:      cty.String,
   383  								Computed:  true,
   384  								Sensitive: true,
   385  							},
   386  							"sensitive_collection": {
   387  								Type:      cty.Map(cty.String),
   388  								Computed:  true,
   389  								Sensitive: true,
   390  							},
   391  						},
   392  					},
   393  				},
   394  			},
   395  		},
   396  	}
   397  
   398  	// The resource we'll inspect
   399  	addr := addrs.Resource{
   400  		Mode: addrs.ManagedResourceMode,
   401  		Type: "test_resource",
   402  		Name: "foo",
   403  	}
   404  	schema, _ := schemas.ResourceTypeConfig(addrs.NewDefaultProvider("test"), addr.Mode, addr.Type)
   405  	// This encoding separates out the After's marks into its AfterValMarks
   406  	csrc, _ := change.Encode(schema.ImpliedType())
   407  	changesSync.AppendResourceInstanceChange(csrc)
   408  
   409  	evaluator := &Evaluator{
   410  		Meta: &ContextMeta{
   411  			Env: "foo",
   412  		},
   413  		Changes: changesSync,
   414  		Config: &configs.Config{
   415  			Module: &configs.Module{
   416  				ManagedResources: map[string]*configs.Resource{
   417  					"test_resource.foo": {
   418  						Mode: addrs.ManagedResourceMode,
   419  						Type: "test_resource",
   420  						Name: "foo",
   421  						Provider: addrs.Provider{
   422  							Hostname:  addrs.DefaultProviderRegistryHost,
   423  							Namespace: "hashicorp",
   424  							Type:      "test",
   425  						},
   426  					},
   427  				},
   428  			},
   429  		},
   430  		State:   stateSync,
   431  		Plugins: schemaOnlyProvidersForTesting(schemas.Providers),
   432  	}
   433  
   434  	data := &evaluationStateData{
   435  		Evaluator: evaluator,
   436  	}
   437  	scope := evaluator.Scope(data, nil)
   438  
   439  	want := cty.ObjectVal(map[string]cty.Value{
   440  		"id":              cty.StringVal("foo"),
   441  		"to_mark_val":     cty.StringVal("pizza").Mark(marks.Sensitive),
   442  		"sensitive_value": cty.StringVal("abc").Mark(marks.Sensitive),
   443  		"sensitive_collection": cty.MapVal(map[string]cty.Value{
   444  			"boop": cty.StringVal("beep"),
   445  		}).Mark(marks.Sensitive),
   446  	})
   447  
   448  	got, diags := scope.Data.GetResource(addr, tfdiags.SourceRange{})
   449  
   450  	if len(diags) != 0 {
   451  		t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
   452  	}
   453  
   454  	if !got.RawEquals(want) {
   455  		t.Errorf("wrong result:\ngot: %#v\nwant: %#v", got, want)
   456  	}
   457  }
   458  
   459  func TestEvaluatorGetModule(t *testing.T) {
   460  	// Create a new evaluator with an existing state
   461  	stateSync := states.BuildState(func(ss *states.SyncState) {
   462  		ss.SetOutputValue(
   463  			addrs.OutputValue{Name: "out"}.Absolute(addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "mod"}}),
   464  			cty.StringVal("bar"),
   465  			true,
   466  		)
   467  	}).SyncWrapper()
   468  	evaluator := evaluatorForModule(stateSync, plans.NewChanges().SyncWrapper())
   469  	data := &evaluationStateData{
   470  		Evaluator: evaluator,
   471  	}
   472  	scope := evaluator.Scope(data, nil)
   473  	want := cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("bar").Mark(marks.Sensitive)})
   474  	got, diags := scope.Data.GetModule(addrs.ModuleCall{
   475  		Name: "mod",
   476  	}, tfdiags.SourceRange{})
   477  
   478  	if len(diags) != 0 {
   479  		t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
   480  	}
   481  	if !got.RawEquals(want) {
   482  		t.Errorf("wrong result %#v; want %#v", got, want)
   483  	}
   484  
   485  	// Changes should override the state value
   486  	changesSync := plans.NewChanges().SyncWrapper()
   487  	change := &plans.OutputChange{
   488  		Addr:      addrs.OutputValue{Name: "out"}.Absolute(addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "mod"}}),
   489  		Sensitive: true,
   490  		Change: plans.Change{
   491  			After: cty.StringVal("baz"),
   492  		},
   493  	}
   494  	cs, _ := change.Encode()
   495  	changesSync.AppendOutputChange(cs)
   496  	evaluator = evaluatorForModule(stateSync, changesSync)
   497  	data = &evaluationStateData{
   498  		Evaluator: evaluator,
   499  	}
   500  	scope = evaluator.Scope(data, nil)
   501  	want = cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("baz").Mark(marks.Sensitive)})
   502  	got, diags = scope.Data.GetModule(addrs.ModuleCall{
   503  		Name: "mod",
   504  	}, tfdiags.SourceRange{})
   505  
   506  	if len(diags) != 0 {
   507  		t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
   508  	}
   509  	if !got.RawEquals(want) {
   510  		t.Errorf("wrong result %#v; want %#v", got, want)
   511  	}
   512  
   513  	// Test changes with empty state
   514  	evaluator = evaluatorForModule(states.NewState().SyncWrapper(), changesSync)
   515  	data = &evaluationStateData{
   516  		Evaluator: evaluator,
   517  	}
   518  	scope = evaluator.Scope(data, nil)
   519  	want = cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("baz").Mark(marks.Sensitive)})
   520  	got, diags = scope.Data.GetModule(addrs.ModuleCall{
   521  		Name: "mod",
   522  	}, tfdiags.SourceRange{})
   523  
   524  	if len(diags) != 0 {
   525  		t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
   526  	}
   527  	if !got.RawEquals(want) {
   528  		t.Errorf("wrong result %#v; want %#v", got, want)
   529  	}
   530  }
   531  
   532  func evaluatorForModule(stateSync *states.SyncState, changesSync *plans.ChangesSync) *Evaluator {
   533  	return &Evaluator{
   534  		Meta: &ContextMeta{
   535  			Env: "foo",
   536  		},
   537  		Config: &configs.Config{
   538  			Module: &configs.Module{
   539  				ModuleCalls: map[string]*configs.ModuleCall{
   540  					"mod": {
   541  						Name: "mod",
   542  					},
   543  				},
   544  			},
   545  			Children: map[string]*configs.Config{
   546  				"mod": {
   547  					Path: addrs.Module{"module.mod"},
   548  					Module: &configs.Module{
   549  						Outputs: map[string]*configs.Output{
   550  							"out": {
   551  								Name:      "out",
   552  								Sensitive: true,
   553  							},
   554  						},
   555  					},
   556  				},
   557  			},
   558  		},
   559  		State:   stateSync,
   560  		Changes: changesSync,
   561  	}
   562  }