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