github.com/opentofu/opentofu@v1.7.1/internal/backend/unparsed_value.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package backend
     7  
     8  import (
     9  	"fmt"
    10  
    11  	"github.com/hashicorp/hcl/v2"
    12  	"github.com/opentofu/opentofu/internal/configs"
    13  	"github.com/opentofu/opentofu/internal/tfdiags"
    14  	"github.com/opentofu/opentofu/internal/tofu"
    15  	"github.com/zclconf/go-cty/cty"
    16  )
    17  
    18  // UnparsedVariableValue represents a variable value provided by the caller
    19  // whose parsing must be deferred until configuration is available.
    20  //
    21  // This exists to allow processing of variable-setting arguments (e.g. in the
    22  // command package) to be separated from parsing (in the backend package).
    23  type UnparsedVariableValue interface {
    24  	// ParseVariableValue information in the provided variable configuration
    25  	// to parse (if necessary) and return the variable value encapsulated in
    26  	// the receiver.
    27  	//
    28  	// If error diagnostics are returned, the resulting value may be invalid
    29  	// or incomplete.
    30  	ParseVariableValue(mode configs.VariableParsingMode) (*tofu.InputValue, tfdiags.Diagnostics)
    31  }
    32  
    33  // ParseUndeclaredVariableValues processes a map of unparsed variable values
    34  // and returns an input values map of the ones not declared in the specified
    35  // declaration map along with detailed diagnostics about values of undeclared
    36  // variables being present, depending on the source of these values. If more
    37  // than two undeclared values are present in file form (config, auto, -var-file)
    38  // the remaining errors are summarized to avoid a massive list of errors.
    39  func ParseUndeclaredVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*configs.Variable) (tofu.InputValues, tfdiags.Diagnostics) {
    40  	var diags tfdiags.Diagnostics
    41  	ret := make(tofu.InputValues, len(vv))
    42  	seenUndeclaredInFile := 0
    43  
    44  	for name, rv := range vv {
    45  		if _, declared := decls[name]; declared {
    46  			// Only interested in parsing undeclared variables
    47  			continue
    48  		}
    49  
    50  		val, valDiags := rv.ParseVariableValue(configs.VariableParseLiteral)
    51  		if valDiags.HasErrors() {
    52  			continue
    53  		}
    54  
    55  		ret[name] = val
    56  
    57  		switch val.SourceType {
    58  		case tofu.ValueFromConfig, tofu.ValueFromAutoFile, tofu.ValueFromNamedFile:
    59  			// We allow undeclared names for variable values from files and warn in case
    60  			// users have forgotten a variable {} declaration or have a typo in their var name.
    61  			// Some users will actively ignore this warning because they use a .tfvars file
    62  			// across multiple configurations.
    63  			if seenUndeclaredInFile < 2 {
    64  				diags = diags.Append(tfdiags.Sourceless(
    65  					tfdiags.Warning,
    66  					"Value for undeclared variable",
    67  					fmt.Sprintf("The root module does not declare a variable named %q but a value was found in file %q. If you meant to use this value, add a \"variable\" block to the configuration.\n\nTo silence these warnings, use TF_VAR_... environment variables to provide certain \"global\" settings to all configurations in your organization. To reduce the verbosity of these warnings, use the -compact-warnings option.", name, val.SourceRange.Filename),
    68  				))
    69  			}
    70  			seenUndeclaredInFile++
    71  
    72  		case tofu.ValueFromEnvVar:
    73  			// We allow and ignore undeclared names for environment
    74  			// variables, because users will often set these globally
    75  			// when they are used across many (but not necessarily all)
    76  			// configurations.
    77  		case tofu.ValueFromCLIArg:
    78  			diags = diags.Append(tfdiags.Sourceless(
    79  				tfdiags.Error,
    80  				"Value for undeclared variable",
    81  				fmt.Sprintf("A variable named %q was assigned on the command line, but the root module does not declare a variable of that name. To use this value, add a \"variable\" block to the configuration.", name),
    82  			))
    83  		default:
    84  			// For all other source types we are more vague, but other situations
    85  			// don't generally crop up at this layer in practice.
    86  			diags = diags.Append(tfdiags.Sourceless(
    87  				tfdiags.Error,
    88  				"Value for undeclared variable",
    89  				fmt.Sprintf("A variable named %q was assigned a value, but the root module does not declare a variable of that name. To use this value, add a \"variable\" block to the configuration.", name),
    90  			))
    91  		}
    92  	}
    93  
    94  	if seenUndeclaredInFile > 2 {
    95  		extras := seenUndeclaredInFile - 2
    96  		diags = diags.Append(&hcl.Diagnostic{
    97  			Severity: hcl.DiagWarning,
    98  			Summary:  "Values for undeclared variables",
    99  			Detail:   fmt.Sprintf("In addition to the other similar warnings shown, %d other variable(s) defined without being declared.", extras),
   100  		})
   101  	}
   102  
   103  	return ret, diags
   104  }
   105  
   106  // ParseDeclaredVariableValues processes a map of unparsed variable values
   107  // and returns an input values map of the ones declared in the specified
   108  // variable declaration mapping. Diagnostics will be populating with
   109  // any variable parsing errors encountered within this collection.
   110  func ParseDeclaredVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*configs.Variable) (tofu.InputValues, tfdiags.Diagnostics) {
   111  	var diags tfdiags.Diagnostics
   112  	ret := make(tofu.InputValues, len(vv))
   113  
   114  	for name, rv := range vv {
   115  		var mode configs.VariableParsingMode
   116  		config, declared := decls[name]
   117  
   118  		if declared {
   119  			mode = config.ParsingMode
   120  		} else {
   121  			// Only interested in parsing declared variables
   122  			continue
   123  		}
   124  
   125  		val, valDiags := rv.ParseVariableValue(mode)
   126  		diags = diags.Append(valDiags)
   127  		if valDiags.HasErrors() {
   128  			continue
   129  		}
   130  
   131  		ret[name] = val
   132  	}
   133  
   134  	return ret, diags
   135  }
   136  
   137  // Checks all given tofu.InputValues variable maps for the existance of
   138  // a named variable
   139  func isDefinedAny(name string, maps ...tofu.InputValues) bool {
   140  	for _, m := range maps {
   141  		if _, defined := m[name]; defined {
   142  			return true
   143  		}
   144  	}
   145  	return false
   146  }
   147  
   148  // ParseVariableValues processes a map of unparsed variable values by
   149  // correlating each one with the given variable declarations which should
   150  // be from a root module.
   151  //
   152  // The map of unparsed variable values should include variables from all
   153  // possible root module declarations sources such that it is as complete as
   154  // it can possibly be for the current operation. If any declared variables
   155  // are not included in the map, ParseVariableValues will either substitute
   156  // a configured default value or produce an error.
   157  //
   158  // If this function returns without any errors in the diagnostics, the
   159  // resulting input values map is guaranteed to be valid and ready to pass
   160  // to tofu.NewContext. If the diagnostics contains errors, the returned
   161  // InputValues may be incomplete but will include the subset of variables
   162  // that were successfully processed, allowing for careful analysis of the
   163  // partial result.
   164  func ParseVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*configs.Variable) (tofu.InputValues, tfdiags.Diagnostics) {
   165  	ret, diags := ParseDeclaredVariableValues(vv, decls)
   166  	undeclared, diagsUndeclared := ParseUndeclaredVariableValues(vv, decls)
   167  
   168  	diags = diags.Append(diagsUndeclared)
   169  
   170  	// By this point we should've gathered all the required root module
   171  	// variables from one of the many possible sources. We'll now populate
   172  	// any we haven't gathered as unset placeholders which OpenTofu Core
   173  	// can then react to.
   174  	for name, vc := range decls {
   175  		if isDefinedAny(name, ret, undeclared) {
   176  			continue
   177  		}
   178  
   179  		// This check is redundant with a check made in OpenTofu Core when
   180  		// processing undeclared variables, but allows us to generate a more
   181  		// specific error message which mentions -var and -var-file command
   182  		// line options, whereas the one in OpenTofu Core is more general
   183  		// due to supporting both root and child module variables.
   184  		if vc.Required() {
   185  			diags = diags.Append(&hcl.Diagnostic{
   186  				Severity: hcl.DiagError,
   187  				Summary:  "No value for required variable",
   188  				Detail:   fmt.Sprintf("The root module input variable %q is not set, and has no default value. Use a -var or -var-file command line argument to provide a value for this variable.", name),
   189  				Subject:  vc.DeclRange.Ptr(),
   190  			})
   191  
   192  			// We'll include a placeholder value anyway, just so that our
   193  			// result is complete for any calling code that wants to cautiously
   194  			// analyze it for diagnostic purposes. Since our diagnostics now
   195  			// includes an error, normal processing will ignore this result.
   196  			ret[name] = &tofu.InputValue{
   197  				Value:       cty.DynamicVal,
   198  				SourceType:  tofu.ValueFromConfig,
   199  				SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange),
   200  			}
   201  		} else {
   202  			// We're still required to put an entry for this variable
   203  			// in the mapping to be explicit to OpenTofu Core that we
   204  			// visited it, but its value will be cty.NilVal to represent
   205  			// that it wasn't set at all at this layer, and so OpenTofu Core
   206  			// should substitute a default if available, or generate an error
   207  			// if not.
   208  			ret[name] = &tofu.InputValue{
   209  				Value:       cty.NilVal,
   210  				SourceType:  tofu.ValueFromConfig,
   211  				SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange),
   212  			}
   213  		}
   214  	}
   215  
   216  	return ret, diags
   217  }