github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/backend/unparsed_value.go (about)

     1  package backend
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/hcl/v2"
     7  	"github.com/hashicorp/terraform/internal/configs"
     8  	"github.com/hashicorp/terraform/internal/terraform"
     9  	"github.com/hashicorp/terraform/internal/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  // ParseVariableValues processes a map of unparsed variable values by
    29  // correlating each one with the given variable declarations which should
    30  // be from a root module.
    31  //
    32  // The map of unparsed variable values should include variables from all
    33  // possible root module declarations sources such that it is as complete as
    34  // it can possibly be for the current operation. If any declared variables
    35  // are not included in the map, ParseVariableValues will either substitute
    36  // a configured default value or produce an error.
    37  //
    38  // If this function returns without any errors in the diagnostics, the
    39  // resulting input values map is guaranteed to be valid and ready to pass
    40  // to terraform.NewContext. If the diagnostics contains errors, the returned
    41  // InputValues may be incomplete but will include the subset of variables
    42  // that were successfully processed, allowing for careful analysis of the
    43  // partial result.
    44  func ParseVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*configs.Variable) (terraform.InputValues, tfdiags.Diagnostics) {
    45  	var diags tfdiags.Diagnostics
    46  	ret := make(terraform.InputValues, len(vv))
    47  
    48  	// Currently we're generating only warnings for undeclared variables
    49  	// defined in files (see below) but we only want to generate a few warnings
    50  	// at a time because existing deployments may have lots of these and
    51  	// the result can therefore be overwhelming.
    52  	seenUndeclaredInFile := 0
    53  
    54  	for name, rv := range vv {
    55  		var mode configs.VariableParsingMode
    56  		config, declared := decls[name]
    57  		if declared {
    58  			mode = config.ParsingMode
    59  		} else {
    60  			mode = configs.VariableParseLiteral
    61  		}
    62  
    63  		val, valDiags := rv.ParseVariableValue(mode)
    64  		diags = diags.Append(valDiags)
    65  		if valDiags.HasErrors() {
    66  			continue
    67  		}
    68  
    69  		if !declared {
    70  			switch val.SourceType {
    71  			case terraform.ValueFromConfig, terraform.ValueFromAutoFile, terraform.ValueFromNamedFile:
    72  				// We allow undeclared names for variable values from files and warn in case
    73  				// users have forgotten a variable {} declaration or have a typo in their var name.
    74  				// Some users will actively ignore this warning because they use a .tfvars file
    75  				// across multiple configurations.
    76  				if seenUndeclaredInFile < 2 {
    77  					diags = diags.Append(tfdiags.Sourceless(
    78  						tfdiags.Warning,
    79  						"Value for undeclared variable",
    80  						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),
    81  					))
    82  				}
    83  				seenUndeclaredInFile++
    84  
    85  			case terraform.ValueFromEnvVar:
    86  				// We allow and ignore undeclared names for environment
    87  				// variables, because users will often set these globally
    88  				// when they are used across many (but not necessarily all)
    89  				// configurations.
    90  			case terraform.ValueFromCLIArg:
    91  				diags = diags.Append(tfdiags.Sourceless(
    92  					tfdiags.Error,
    93  					"Value for undeclared variable",
    94  					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),
    95  				))
    96  			default:
    97  				// For all other source types we are more vague, but other situations
    98  				// don't generally crop up at this layer in practice.
    99  				diags = diags.Append(tfdiags.Sourceless(
   100  					tfdiags.Error,
   101  					"Value for undeclared variable",
   102  					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),
   103  				))
   104  			}
   105  			continue
   106  		}
   107  
   108  		ret[name] = val
   109  	}
   110  
   111  	if seenUndeclaredInFile > 2 {
   112  		extras := seenUndeclaredInFile - 2
   113  		diags = diags.Append(&hcl.Diagnostic{
   114  			Severity: hcl.DiagWarning,
   115  			Summary:  "Values for undeclared variables",
   116  			Detail:   fmt.Sprintf("In addition to the other similar warnings shown, %d other variable(s) defined without being declared.", extras),
   117  		})
   118  	}
   119  
   120  	// By this point we should've gathered all of the required root module
   121  	// variables from one of the many possible sources. We'll now populate
   122  	// any we haven't gathered as their defaults and fail if any of the
   123  	// missing ones are required.
   124  	for name, vc := range decls {
   125  		if _, defined := ret[name]; defined {
   126  			continue
   127  		}
   128  
   129  		if vc.Required() {
   130  			diags = diags.Append(&hcl.Diagnostic{
   131  				Severity: hcl.DiagError,
   132  				Summary:  "No value for required variable",
   133  				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),
   134  				Subject:  vc.DeclRange.Ptr(),
   135  			})
   136  
   137  			// We'll include a placeholder value anyway, just so that our
   138  			// result is complete for any calling code that wants to cautiously
   139  			// analyze it for diagnostic purposes. Since our diagnostics now
   140  			// includes an error, normal processing will ignore this result.
   141  			ret[name] = &terraform.InputValue{
   142  				Value:       cty.DynamicVal,
   143  				SourceType:  terraform.ValueFromConfig,
   144  				SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange),
   145  			}
   146  		} else {
   147  			ret[name] = &terraform.InputValue{
   148  				Value:       vc.Default,
   149  				SourceType:  terraform.ValueFromConfig,
   150  				SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange),
   151  			}
   152  		}
   153  	}
   154  
   155  	return ret, diags
   156  }