github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/unparsed_value.go (about)

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