github.com/kevinklinger/open_terraform@v1.3.6/noninternal/backend/unparsed_value.go (about)

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