Allowed variable to pass inside variables block (#1488)

Signed-off-by: siddharthasonker95 <158144589+siddharthasonker95@users.noreply.github.com>
This commit is contained in:
Siddhartha Sonker 2024-06-03 18:44:05 +05:30 committed by GitHub
parent 1fecaef9aa
commit 9138470a67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 204 additions and 95 deletions

View File

@ -9,6 +9,7 @@ ENHANCEMENTS:
* Made `tofu plan` with `generate-config-out` flag replace JSON strings with `jsonencode` functions calls. ([#1595](https://github.com/opentofu/opentofu/pull/1595))
* Make state persistence interval configurable via `TF_STATE_PERSIST_INTERVAL` environment variable ([#1591](https://github.com/opentofu/opentofu/pull/1591))
* Improved performance of writing state files and reduced their size using compact json encoding. ([#1647](https://github.com/opentofu/opentofu/pull/1647))
* Allow to reference variable inside the `variables` block of a test file. ([1488](https://github.com/opentofu/opentofu/pull/1488))
BUG FIXES:
* Fixed crash in gcs backend when using certain commands ([#1618](https://github.com/opentofu/opentofu/pull/1618))

View File

@ -668,7 +668,10 @@ func (runner *TestFileRunner) destroy(config *configs.Config, state *states.Stat
var diags tfdiags.Diagnostics
variables, variableDiags := buildInputVariablesForTest(run, file, config, runner.Suite.GlobalVariables, runner.States)
evalCtx, ctxDiags := getEvalContextForTest(runner.States, config, runner.Suite.GlobalVariables)
diags = diags.Append(ctxDiags)
variables, variableDiags := buildInputVariablesForTest(run, file, config, runner.Suite.GlobalVariables, evalCtx)
diags = diags.Append(variableDiags)
if diags.HasErrors() {
@ -731,7 +734,10 @@ func (runner *TestFileRunner) plan(config *configs.Config, state *states.State,
references, referenceDiags := run.GetReferences()
diags = diags.Append(referenceDiags)
variables, variableDiags := buildInputVariablesForTest(run, file, config, runner.Suite.GlobalVariables, runner.States)
evalCtx, ctxDiags := getEvalContextForTest(runner.States, config, runner.Suite.GlobalVariables)
diags = diags.Append(ctxDiags)
variables, variableDiags := buildInputVariablesForTest(run, file, config, runner.Suite.GlobalVariables, evalCtx)
diags = diags.Append(variableDiags)
if diags.HasErrors() {
@ -1000,9 +1006,8 @@ func (runner *TestFileRunner) Cleanup(file *moduletest.File) {
// Crucially, it differs from prepareInputVariablesForAssertions in that it only
// includes variables that are reference by the config and not everything that
// is defined within the test run block and test file.
func buildInputVariablesForTest(run *moduletest.Run, file *moduletest.File, config *configs.Config, globals map[string]backend.UnparsedVariableValue, states map[string]*TestFileState) (tofu.InputValues, tfdiags.Diagnostics) {
func buildInputVariablesForTest(run *moduletest.Run, file *moduletest.File, config *configs.Config, globals map[string]backend.UnparsedVariableValue, evalCtx *hcl.EvalContext) (tofu.InputValues, tfdiags.Diagnostics) {
variables := make(map[string]backend.UnparsedVariableValue)
evalCtx := getEvalContextFromStates(states)
for name := range config.Module.Variables {
if run != nil {
if expr, exists := run.Config.Variables[name]; exists {
@ -1019,9 +1024,10 @@ func buildInputVariablesForTest(run *moduletest.Run, file *moduletest.File, conf
if file != nil {
if expr, exists := file.Config.Variables[name]; exists {
// If it's not set locally, it maybe set for the entire file.
variables[name] = unparsedVariableValueExpression{
variables[name] = testVariableValueExpression{
expr: expr,
sourceType: tofu.ValueFromConfig,
ctx: evalCtx,
}
continue
}
@ -1042,17 +1048,12 @@ func buildInputVariablesForTest(run *moduletest.Run, file *moduletest.File, conf
return backend.ParseVariableValues(variables, config.Module.Variables)
}
// getEvalContextFromStates constructs an hcl.EvalContext based on the provided map
// of TestFileState instances. It extracts the relevant information from the
// states to create a context suitable for HCL evaluation, including the output
// values of modules.
//
// Parameters:
// - states: A map of TestFileState instances containing the state information.
//
// Returns:
// - *hcl.EvalContext: The constructed HCL evaluation context.
func getEvalContextFromStates(states map[string]*TestFileState) *hcl.EvalContext {
// getEvalContextForTest constructs an hcl.EvalContext based on the provided map of
// TestFileState instances, configuration and global variables.
// It extracts the relevant information from the input parameters to create a
// context suitable for HCL evaluation.
func getEvalContextForTest(states map[string]*TestFileState, config *configs.Config, globals map[string]backend.UnparsedVariableValue) (*hcl.EvalContext, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
runCtx := make(map[string]cty.Value)
for _, state := range states {
if state.Run == nil {
@ -1065,9 +1066,24 @@ func getEvalContextFromStates(states map[string]*TestFileState) *hcl.EvalContext
}
runCtx[state.Run.Name] = cty.ObjectVal(outputs)
}
ctx := &hcl.EvalContext{Variables: map[string]cty.Value{"run": cty.ObjectVal(runCtx)}}
return ctx
// If the variable is referenced in the tfvars file or TF_VAR_ environment variable, then lookup the value
// in global variables; otherwise, assign the default value.
inputValues, diags := parseAndApplyDefaultValues(globals, config.Module.Variables)
diags.Append(diags)
varCtx := make(map[string]cty.Value)
for name, val := range inputValues {
varCtx[name] = val.Value
}
ctx := &hcl.EvalContext{
Variables: map[string]cty.Value{
"run": cty.ObjectVal(runCtx),
"var": cty.ObjectVal(varCtx),
},
}
return ctx, diags
}
type testVariableValueExpression struct {
@ -1090,6 +1106,40 @@ func (v testVariableValueExpression) ParseVariableValue(mode configs.VariablePar
}, diags
}
// parseAndApplyDefaultValues parses the given unparsed variables into tofu.InputValues
// and applies default values from the configuration variables where applicable.
// This ensures all variables are correctly initialized and returns the resulting tofu.InputValues.
func parseAndApplyDefaultValues(unparsedVariables map[string]backend.UnparsedVariableValue, configVariables map[string]*configs.Variable) (tofu.InputValues, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
inputs := make(tofu.InputValues, len(unparsedVariables))
for name, variable := range unparsedVariables {
value, valueDiags := variable.ParseVariableValue(configs.VariableParseLiteral)
diags = diags.Append(valueDiags)
inputs[name] = value
}
// Now, we're going to apply any default values from the configuration.
// We do this after the conversion into tofu.InputValues, as the
// defaults have already been converted into cty.Value objects.
for name, variable := range configVariables {
if _, exists := unparsedVariables[name]; exists {
// Then we don't want to apply the default for this variable as we
// already have a value.
continue
}
if variable.Default != cty.NilVal {
inputs[name] = &tofu.InputValue{
Value: variable.Default,
SourceType: tofu.ValueFromConfig,
SourceRange: tfdiags.SourceRangeFromHCL(variable.DeclRange),
}
}
}
return inputs, diags
}
// prepareInputVariablesForAssertions creates a tofu.InputValues mapping
// that contains all the variables defined for a given run and file, alongside
// any unset variables that have defaults within the provided config.
@ -1107,7 +1157,9 @@ func (v testVariableValueExpression) ParseVariableValue(mode configs.VariablePar
// available are also defined in the config. It returns a function that resets
// the config which must be called so the config can be reused going forward.
func (runner *TestFileRunner) prepareInputVariablesForAssertions(config *configs.Config, run *moduletest.Run, file *moduletest.File, globals map[string]backend.UnparsedVariableValue) (tofu.InputValues, func(), tfdiags.Diagnostics) {
ctx := getEvalContextFromStates(runner.States)
var diags tfdiags.Diagnostics
ctx, ctxDiags := getEvalContextForTest(runner.States, config, globals)
diags = diags.Append(ctxDiags)
variables := make(map[string]backend.UnparsedVariableValue)
@ -1148,34 +1200,9 @@ func (runner *TestFileRunner) prepareInputVariablesForAssertions(config *configs
// We've gathered all the values we have, let's convert them into
// tofu.InputValues so they can be passed into the OpenTofu graph.
inputs := make(tofu.InputValues, len(variables))
var diags tfdiags.Diagnostics
for name, variable := range variables {
value, valueDiags := variable.ParseVariableValue(configs.VariableParseLiteral)
diags = diags.Append(valueDiags)
inputs[name] = value
}
// Next, we're going to apply any default values from the configuration.
// We do this after the conversion into tofu.InputValues, as the
// defaults have already been converted into cty.Value objects.
for name, variable := range config.Module.Variables {
if _, exists := variables[name]; exists {
// Then we don't want to apply the default for this variable as we
// already have a value.
continue
}
if variable.Default != cty.NilVal {
inputs[name] = &tofu.InputValue{
Value: variable.Default,
SourceType: tofu.ValueFromConfig,
SourceRange: tfdiags.SourceRangeFromHCL(variable.DeclRange),
}
}
}
// Also, apply default values from the configuration variables where applicable.
inputs, valDiags := parseAndApplyDefaultValues(variables, config.Module.Variables)
diags.Append(valDiags)
// Finally, we're going to do a some modifications to the config.
// If we have got variable values from the test file we need to make sure

View File

@ -1132,48 +1132,13 @@ Condition expression could not be evaluated at this time.
}
func TestTest_LocalVariables(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "pass_with_local_variable")), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer close()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
c := &TestCommand{
Meta: meta,
}
code := c.Run([]string{"-verbose", "-no-color"})
output := done(t)
if code != 0 {
t.Errorf("expected status code 0 but got %d", code)
}
expected := `tests/test.tftest.hcl... pass
tcs := map[string]struct {
expected string
code int
skip bool
}{
"pass_with_local_variable": {
expected: `tests/test.tftest.hcl... pass
run "first"... pass
@ -1188,16 +1153,91 @@ OpenTofu has compared your real infrastructure against your configuration and
found no differences, so no changes are needed.
Success! 2 passed, 0 failed.
`
`,
code: 0,
},
"pass_var_inside_variables": {
expected: `main.tftest.hcl... pass
run "first"... pass
actual := output.All()
if diff := cmp.Diff(actual, expected); len(diff) > 0 {
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff)
Outputs:
sss = "false"
Success! 1 passed, 0 failed.
`,
code: 0,
},
"pass_var_with_default_value_inside_variables": {
expected: `main.tftest.hcl... pass
run "first"... pass
Outputs:
sss = "true"
Success! 1 passed, 0 failed.
`,
code: 0,
},
}
if provider.ResourceCount() > 0 {
t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
if tc.skip {
t.Skip()
}
file := name
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", file)), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
providerSource, providerClose := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer providerClose()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
command := &TestCommand{
Meta: meta,
}
code := command.Run([]string{"-verbose", "-no-color"})
output := done(t)
if code != tc.code {
t.Errorf("expected status code %d but got %d: %s", tc.code, code, output.All())
}
actual := output.All()
if diff := cmp.Diff(actual, tc.expected); len(diff) > 0 {
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", tc.expected, actual, diff)
}
})
}
}

View File

@ -0,0 +1,10 @@
variable "var1" {
default = true
}
variable "var2" {
}
output "sss" {
value = var.var2
}

View File

@ -0,0 +1,10 @@
variables {
var2 = var.var1 ? "true" : "false"
}
run "first" {
assert {
condition = output.sss == "false"
error_message = "Should work"
}
}

View File

@ -0,0 +1 @@
var1 = false

View File

@ -0,0 +1,10 @@
variable "var1" {
default = true
}
variable "var2" {
}
output "sss" {
value = var.var2
}

View File

@ -0,0 +1,10 @@
variables {
var2 = var.var1 ? "true" : "false"
}
run "first" {
assert {
condition = output.sss == "true"
error_message = "Should work"
}
}